refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AIController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_status'],
|
||||
'permission_callback' => [$this, 'check_logged_in'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-generate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_generate'],
|
||||
'permission_callback' => [$this, 'check_logged_in'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_suggest'],
|
||||
'permission_callback' => [$this, 'check_write_assist'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'summary_suggest'],
|
||||
'permission_callback' => [$this, 'check_write_assist'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/tags-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'tags_suggest'],
|
||||
'permission_callback' => [$this, 'check_write_assist'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_logged_in(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
/**
|
||||
* 投稿/编辑写作辅助:已登录且可编辑文章或允许前台投稿。
|
||||
*/
|
||||
public function check_write_assist(): bool {
|
||||
if (!is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
if (current_user_can('edit_posts')) {
|
||||
return true;
|
||||
}
|
||||
if (class_exists('\AKSRT\Core\FrontSubmit')) {
|
||||
return (bool) \AKSRT\Core\FrontSubmit::get_instance()->can_submit();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_status(): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
||||
return $this->success(['data' => [
|
||||
'enabled' => false,
|
||||
'provider' => 'none',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
'backend' => 'none',
|
||||
]]);
|
||||
}
|
||||
|
||||
return $this->success(['data' => \AKSRT\Ai\AI::get_instance()->get_status()]);
|
||||
}
|
||||
|
||||
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$allowed = [
|
||||
'enabled' => 'bool',
|
||||
'provider' => 'string',
|
||||
'api_key' => 'string',
|
||||
'api_base' => 'url',
|
||||
'model' => 'string',
|
||||
'max_tokens' => 'int',
|
||||
'rate_limit' => 'int',
|
||||
];
|
||||
|
||||
$config = [];
|
||||
foreach ($allowed as $key => $type) {
|
||||
if (!isset($params[$key])) {
|
||||
continue;
|
||||
}
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
$config[$key] = !empty($params[$key]);
|
||||
break;
|
||||
case 'int':
|
||||
$config[$key] = (int) $params[$key];
|
||||
break;
|
||||
case 'url':
|
||||
$config[$key] = esc_url_raw((string) $params[$key]);
|
||||
break;
|
||||
default:
|
||||
$config[$key] = sanitize_text_field((string) $params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$success = \AKSRT\Ai\AI::get_instance()->update_config($config);
|
||||
if ($success) {
|
||||
return $this->success([
|
||||
'message' => __('AI 设置已保存', 'aksrt'),
|
||||
'data' => \AKSRT\Ai\AI::get_instance()->get_status(),
|
||||
]);
|
||||
}
|
||||
return $this->error(__('保存失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/seo-generate — 从 post/term 实体生成 SEO 三字段。
|
||||
*/
|
||||
public function seo_generate(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'post'));
|
||||
$id = absint($params['id'] ?? 0);
|
||||
|
||||
if (!in_array($type, ['post', 'term'], true) || $id <= 0) {
|
||||
return $this->error(__('参数错误', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if ($type === 'post') {
|
||||
if (!current_user_can('edit_post', $id)) {
|
||||
return $this->error(__('权限不足,无法编辑此文章', 'aksrt'), 403);
|
||||
}
|
||||
} else {
|
||||
if (!current_user_can('edit_term', $id)) {
|
||||
return $this->error(__('权限不足,无法编辑此分类/标签', 'aksrt'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
$result = \AKSRT\Ai\AISEO::get_instance()->generate_fields($type, $id);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function seo_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AISEO')) {
|
||||
return $this->error(__('AI SEO 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
$post_id = absint($params['post_id'] ?? 0);
|
||||
|
||||
if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
|
||||
return $this->error(__('权限不足', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
if ($title === '') {
|
||||
return $this->error(__('标题不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$result = \AKSRT\Ai\AISEO::get_instance()->suggest($title, $content);
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
||||
}
|
||||
|
||||
public function summary_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
// Accept JSON body or form body (some admin contexts strip JSON).
|
||||
if ($params === []) {
|
||||
$params = $request->get_params();
|
||||
if (!is_array($params)) {
|
||||
$params = [];
|
||||
}
|
||||
}
|
||||
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$raw = (string) ($params['content'] ?? '');
|
||||
// Keep readable text; strip tags for emptiness / prompt length.
|
||||
$plain = trim(wp_strip_all_tags(html_entity_decode($raw, ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
||||
$content = $plain !== '' ? $plain : trim(wp_strip_all_tags(wp_kses_post($raw)));
|
||||
$max_len = max(50, min(500, (int) ($params['max_length'] ?? 200)));
|
||||
|
||||
if ($content === '') {
|
||||
return $this->error(__('内容不能为空,请先在编辑器填写正文后再生成摘要', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
$ai = \AKSRT\Ai\AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return $this->error(
|
||||
__('AI 服务未启用或未配置。请到「扩展 → AI 辅助」启用并填写 API Key(或配置 WordPress AI Connectors)。', 'aksrt'),
|
||||
503
|
||||
);
|
||||
}
|
||||
|
||||
// Prefer excerpt-oriented prompt when title is present.
|
||||
$prompt_builder = \AKSRT\Ai\AIPrompt::get_instance();
|
||||
if ($title !== '' && method_exists($prompt_builder, 'build_excerpt_prompt')) {
|
||||
$prompt = $prompt_builder->build_excerpt_prompt($title, $content);
|
||||
} else {
|
||||
$prompt = $prompt_builder->build_summary_prompt($content, $max_len);
|
||||
}
|
||||
|
||||
$result = $ai->generate_text($prompt, [
|
||||
'max_tokens' => min(600, max(120, $max_len * 2)),
|
||||
'temperature' => 0.5,
|
||||
'system_instruction' => __('你是中文内容编辑,只输出摘要正文,不要标题、前缀或 Markdown。', 'aksrt'),
|
||||
]);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$summary = trim((string) ($result['content'] ?? ''));
|
||||
if ($summary === '') {
|
||||
return $this->error(__('AI 返回内容为空,请稍后重试或检查模型配置', 'aksrt'), 502);
|
||||
}
|
||||
// Soft trim to requested length (characters).
|
||||
if (function_exists('mb_substr') && mb_strlen($summary) > $max_len + 20) {
|
||||
$summary = mb_substr($summary, 0, $max_len);
|
||||
}
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'summary' => $summary,
|
||||
'backend' => (string) ($result['backend'] ?? ''),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$message = trim((string) ($result['message'] ?? ''));
|
||||
if ($message === '') {
|
||||
$message = __('生成失败', 'aksrt');
|
||||
}
|
||||
|
||||
// Upstream/provider failures are not client validation errors.
|
||||
$status = 502;
|
||||
if (str_contains($message, '未启用') || str_contains($message, '未配置')) {
|
||||
$status = 503;
|
||||
} elseif (str_contains($message, '频繁')) {
|
||||
$status = 429;
|
||||
}
|
||||
|
||||
return $this->error($message, $status);
|
||||
}
|
||||
|
||||
public function tags_suggest(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Ai\AI') || !class_exists('\AKSRT\Ai\AIPrompt')) {
|
||||
return $this->error(__('AI 模块未加载', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
|
||||
if ($title === '' && trim(wp_strip_all_tags($content)) === '') {
|
||||
return $this->error(__('标题或内容不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$prompt = \AKSRT\Ai\AIPrompt::get_instance()->build_tags_prompt($title, $content);
|
||||
$result = \AKSRT\Ai\AI::get_instance()->generate_text($prompt, [
|
||||
'max_tokens' => 300,
|
||||
'temperature' => 0.5,
|
||||
]);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$tags = json_decode((string) $result['content'], true);
|
||||
if (!is_array($tags)) {
|
||||
$tags = array_filter(array_map('trim', explode(',', (string) $result['content'])));
|
||||
}
|
||||
$tags = array_values(array_map('sanitize_text_field', $tags));
|
||||
return $this->success(['data' => ['tags' => $tags]]);
|
||||
}
|
||||
|
||||
return $this->error((string) ($result['message'] ?? __('生成失败', 'aksrt')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Admin {
|
||||
|
||||
private static $instance = null;
|
||||
private const PAGE_SLUG = 'aksrt-settings';
|
||||
private const API_NAMESPACE = 'aksrt/v1';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public function init(): void {
|
||||
add_action('admin_menu', [$this, 'add_admin_menu']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
|
||||
add_filter('script_loader_tag', [$this, 'add_module_type_attribute'], 10, 2);
|
||||
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
||||
|
||||
add_action('wp_ajax_aksrt_load_more_posts', [$this, 'ajax_load_more_posts']);
|
||||
add_action('wp_ajax_nopriv_aksrt_load_more_posts', [$this, 'ajax_load_more_posts']);
|
||||
add_action('wp_ajax_aksrt_load_more_comments', [$this, 'ajax_load_more_comments']);
|
||||
add_action('wp_ajax_nopriv_aksrt_load_more_comments', [$this, 'ajax_load_more_comments']);
|
||||
}
|
||||
|
||||
public function add_admin_menu(): void {
|
||||
add_menu_page(
|
||||
'AKSRT 主题设置',
|
||||
'AKSRT',
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
[$this, 'render_admin_page'],
|
||||
'dashicons-admin-generic',
|
||||
58
|
||||
);
|
||||
}
|
||||
|
||||
public function render_admin_page(): void {
|
||||
echo '<div class="wrap aksrt-admin-wrap">';
|
||||
echo '<div id="aksrt-admin-app"></div>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function enqueue_admin_assets($hook): void {
|
||||
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||
if (!$screen || $screen->base !== 'toplevel_page_' . self::PAGE_SLUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
$build_dir = trailingslashit(AKSRT_DIR) . 'assets/admin/build/';
|
||||
$build_uri = trailingslashit(AKSRT_URI) . 'assets/admin/build/';
|
||||
$entry = $build_dir . 'index.js';
|
||||
|
||||
if (!is_file($entry)) {
|
||||
add_action('admin_notices', static function (): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="notice notice-error"><p>';
|
||||
echo esc_html__('AKSRT 管理后台构建缺失:请在 assets/admin 执行 npm run build,并完整部署 build/ 目录。', 'aksrt');
|
||||
echo '</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$missing_chunks = self::find_missing_admin_chunks($build_dir, $entry);
|
||||
if ($missing_chunks !== []) {
|
||||
add_action('admin_notices', static function () use ($missing_chunks): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="notice notice-error"><p>';
|
||||
echo esc_html__('AKSRT 管理后台构建不完整(动态模块缺失),请重新完整部署 assets/admin/build/,不要只覆盖 index.js。缺失:', 'aksrt');
|
||||
echo ' ' . esc_html(implode(', ', array_slice($missing_chunks, 0, 8)));
|
||||
if (count($missing_chunks) > 8) {
|
||||
echo '…';
|
||||
}
|
||||
echo '</p></div>';
|
||||
});
|
||||
// Still enqueue entry so console shows the real import error if an admin insists;
|
||||
// notice makes the deploy mistake explicit.
|
||||
}
|
||||
|
||||
$css_files = [
|
||||
'naive-ui.css',
|
||||
'index.css',
|
||||
'index2.css',
|
||||
];
|
||||
foreach ($css_files as $css_file) {
|
||||
$path = $build_dir . $css_file;
|
||||
if (file_exists($path)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-admin-' . sanitize_key(str_replace(['.', '/'], '-', $css_file)),
|
||||
$build_uri . $css_file,
|
||||
[],
|
||||
(string) filemtime($path)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Match frontend FA5 pack so FaIcon / menu glyphs render in the SPA.
|
||||
wp_enqueue_style(
|
||||
'aksrt-font-awesome-5',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css',
|
||||
[],
|
||||
'5.15.4'
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'aksrt-admin-app',
|
||||
$build_uri . 'index.js',
|
||||
[],
|
||||
(string) filemtime($entry),
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('aksrt-admin-app', 'aksrtAdminData', [
|
||||
'apiUrl' => esc_url_raw(rest_url(self::API_NAMESPACE)),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'siteUrl' => home_url('/'),
|
||||
'adminUrl' => admin_url('/'),
|
||||
'themeVersion' => wp_get_theme()->get('Version') ?: '3.4.0-dev',
|
||||
'themeUri' => defined('AKSRT_URI') ? AKSRT_URI : get_template_directory_uri(),
|
||||
'buildOk' => $missing_chunks === [],
|
||||
]);
|
||||
|
||||
wp_enqueue_media();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse index.js dynamic imports and report missing chunk files.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function find_missing_admin_chunks(string $build_dir, string $entry_file): array {
|
||||
$src = file_get_contents($entry_file);
|
||||
if (!is_string($src) || $src === '') {
|
||||
return ['index.js unreadable'];
|
||||
}
|
||||
|
||||
if (!preg_match_all('/\.\/([A-Za-z0-9_.-]+\.js)/', $src, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$missing = [];
|
||||
foreach (array_unique($matches[1]) as $chunk) {
|
||||
if (!is_file($build_dir . $chunk)) {
|
||||
$missing[] = $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public function add_module_type_attribute(string $tag, string $handle): string {
|
||||
if ('aksrt-admin-app' === $handle) {
|
||||
return str_replace('<script ', '<script type="module" ', $tag);
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function register_rest_routes(): void {
|
||||
// FQCN required after PSR-4 move into AKSRT\Admin (unqualified class_exists skips all routes).
|
||||
$controllers = [
|
||||
GlobalController::class,
|
||||
PageDisplayController::class,
|
||||
PostListController::class,
|
||||
PermissionsController::class,
|
||||
UserController::class,
|
||||
PaymentController::class,
|
||||
ToolsController::class,
|
||||
ImportExportController::class,
|
||||
ExtensionController::class,
|
||||
CommentController::class,
|
||||
MessageController::class,
|
||||
AIController::class,
|
||||
];
|
||||
|
||||
foreach ($controllers as $class_name) {
|
||||
if (!class_exists($class_name) || !method_exists($class_name, 'get_instance')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$controller = $class_name::get_instance();
|
||||
if (method_exists($controller, 'register_routes')) {
|
||||
$controller->register_routes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function sanitize_public_query_vars(array $vars): array {
|
||||
$allowed = [
|
||||
'post_type', 'post_status', 'posts_per_page', 'paged', 'orderby', 'order',
|
||||
'cat', 'tag', 'tag_id', 'category_name', 's', 'post__not_in', 'category__not_in',
|
||||
];
|
||||
$clean = [];
|
||||
foreach ($allowed as $key) {
|
||||
if (!array_key_exists($key, $vars)) {
|
||||
continue;
|
||||
}
|
||||
$value = $vars[$key];
|
||||
if (is_array($value)) {
|
||||
$clean[$key] = array_map('sanitize_text_field', $value);
|
||||
} else {
|
||||
$clean[$key] = sanitize_text_field((string) $value);
|
||||
}
|
||||
}
|
||||
if (isset($clean['posts_per_page'])) {
|
||||
$clean['posts_per_page'] = max(1, min(50, (int) $clean['posts_per_page']));
|
||||
}
|
||||
if (isset($clean['paged'])) {
|
||||
$clean['paged'] = max(1, (int) $clean['paged']);
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
public static function sanitize_public_render_config(array $config): array {
|
||||
$clean = [];
|
||||
if (isset($config['mode'])) {
|
||||
$mode = sanitize_key((string) $config['mode']);
|
||||
if (in_array($mode, ['list', 'card', 'mini', 'grid-cms'], true)) {
|
||||
$clean['mode'] = $mode;
|
||||
}
|
||||
}
|
||||
if (isset($config['context'])) {
|
||||
$clean['context'] = sanitize_key((string) $config['context']);
|
||||
}
|
||||
if (isset($config['query_context'])) {
|
||||
$clean['query_context'] = sanitize_key((string) $config['query_context']);
|
||||
}
|
||||
if (isset($config['style'])) {
|
||||
$style = sanitize_key((string) $config['style']);
|
||||
if (in_array($style, ['', 'style2', 'style3'], true)) {
|
||||
$clean['style'] = $style;
|
||||
}
|
||||
}
|
||||
if (isset($config['card_style'])) {
|
||||
$card_style = sanitize_key((string) $config['card_style']);
|
||||
$allowed = ['thumb', 'text', 'thumb_if_has', 'card', 'compact', 'large_thumb', 'multi_thumb', 'standard'];
|
||||
if (in_array($card_style, $allowed, true)) {
|
||||
$clean['card_style'] = $card_style;
|
||||
}
|
||||
}
|
||||
if (isset($config['list_show_type'])) {
|
||||
$v = sanitize_key((string) $config['list_show_type']);
|
||||
if (in_array($v, ['separate', 'no_margin'], true)) {
|
||||
$clean['list_show_type'] = $v;
|
||||
}
|
||||
}
|
||||
if (isset($config['list_img_position'])) {
|
||||
$v = sanitize_key((string) $config['list_img_position']);
|
||||
if (in_array($v, ['left', 'right'], true)) {
|
||||
$clean['list_img_position'] = $v;
|
||||
}
|
||||
}
|
||||
foreach (['thumb_if_has', 'show_thumb', 'show_author', 'show_date', 'show_views', 'show_comments', 'show_likes', 'show_excerpt', 'heading_bold', 'target_blank', 'badge_show', 'multi_thumb_enabled', 'skip_wrapper'] as $bool_key) {
|
||||
if (array_key_exists($bool_key, $config)) {
|
||||
$clean[$bool_key] = !empty($config[$bool_key]);
|
||||
}
|
||||
}
|
||||
if (isset($config['multi_thumb_limit'])) {
|
||||
$clean['multi_thumb_limit'] = max(1, min(6, (int) $config['multi_thumb_limit']));
|
||||
}
|
||||
if (isset($config['multi_thumb_min'])) {
|
||||
$clean['multi_thumb_min'] = max(1, min(6, (int) $config['multi_thumb_min']));
|
||||
}
|
||||
if (isset($config['excerpt_words'])) {
|
||||
$clean['excerpt_words'] = max(0, min(200, (int) $config['excerpt_words']));
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
|
||||
public function ajax_load_more_posts(): void {
|
||||
$nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : '';
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$page = max(1, (int) ($_POST['page'] ?? 1));
|
||||
$query_vars = json_decode(wp_unslash((string) ($_POST['query_vars'] ?? '{}')), true);
|
||||
$render_config = json_decode(wp_unslash((string) ($_POST['render_config'] ?? '{}')), true);
|
||||
$query_vars = is_array($query_vars) ? self::sanitize_public_query_vars($query_vars) : [];
|
||||
$render_config = is_array($render_config) ? self::sanitize_public_render_config($render_config) : [];
|
||||
// Prefer server-side resolution so AJAX matches SSR styles even if client config is slim.
|
||||
if (function_exists('aksrt_resolve_post_list_render_config')) {
|
||||
$context = sanitize_key((string) ($render_config['context'] ?? 'archive'));
|
||||
if ($context === '') {
|
||||
$context = 'archive';
|
||||
}
|
||||
$resolved = aksrt_resolve_post_list_render_config([
|
||||
'context' => $context,
|
||||
'overrides' => $render_config,
|
||||
]);
|
||||
$render_config = array_merge($resolved, $render_config);
|
||||
}
|
||||
$query_vars['paged'] = $page;
|
||||
if (empty($query_vars['post_status'])) {
|
||||
$query_vars['post_status'] = 'publish';
|
||||
}
|
||||
if (empty($query_vars['post_type'])) {
|
||||
$query_vars['post_type'] = 'post';
|
||||
}
|
||||
|
||||
$query = new \WP_Query($query_vars);
|
||||
ob_start();
|
||||
if (function_exists('aksrt_render_post_list')) {
|
||||
aksrt_render_post_list($query, $render_config, true);
|
||||
}
|
||||
$html = (string) ob_get_clean();
|
||||
|
||||
wp_send_json_success([
|
||||
'html' => $html,
|
||||
'has_more' => $page < (int) $query->max_num_pages,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
public function ajax_load_more_comments(): void {
|
||||
$nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : '';
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$post_id = absint($_POST['post_id'] ?? 0);
|
||||
$page = max(1, (int) ($_POST['page'] ?? 1));
|
||||
if ($post_id <= 0) {
|
||||
wp_send_json_error(['message' => __('无效的文章 ID', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
$order = sanitize_key((string) ($_POST['comment_order'] ?? $_POST['order'] ?? 'newest'));
|
||||
$author_only = !empty($_POST['author_only']) || (isset($_POST['aksrt_author_only']) && (int) $_POST['aksrt_author_only'] === 1);
|
||||
|
||||
if (!function_exists('aksrt_comments_query_for_post') || !function_exists('aksrt_comments_render_items_html')) {
|
||||
wp_send_json_error(['message' => __('评论加载不可用', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = aksrt_comments_query_for_post($post_id, [
|
||||
'page' => $page,
|
||||
'order' => $order,
|
||||
'author_only' => $author_only,
|
||||
]);
|
||||
|
||||
$html = aksrt_comments_render_items_html($result['comments'], $result['settings'], $result['order']);
|
||||
|
||||
wp_send_json_success([
|
||||
'html' => $html,
|
||||
'has_more' => $page < (int) $result['max_pages'],
|
||||
'page' => $page,
|
||||
'order' => $result['order'],
|
||||
'total' => (int) $result['total'],
|
||||
'max_pages'=> (int) $result['max_pages'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class CommentController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// List comments (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'list_comments'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
|
||||
'post_id' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'status' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'search' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create comment (admin or frontend)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_comment'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
'args' => [
|
||||
'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
|
||||
'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
'parent' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Update comment status (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)/status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_comment_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
|
||||
'status' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Edit comment content (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_comment'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
|
||||
'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Delete comment (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_comment'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'id' => ['required' => true, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Bulk action (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments/bulk', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'bulk_action'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Frontend: list comments for a post (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/comments/post/(?P<post_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_post_comments'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'post_id' => ['required' => true, 'sanitize_callback' => 'absint'],
|
||||
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['default' => 10, 'sanitize_callback' => 'absint'],
|
||||
'order' => ['default' => 'DESC', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /comments — List comments with filtering.
|
||||
*/
|
||||
public function list_comments(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$args = [
|
||||
'number' => min(100, (int) $request->get_param('per_page')),
|
||||
'offset' => ((int) $request->get_param('page') - 1) * (int) $request->get_param('per_page'),
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
if ($post_id > 0) {
|
||||
$args['post_id'] = $post_id;
|
||||
}
|
||||
|
||||
$status = $request->get_param('status');
|
||||
if ($status !== '') {
|
||||
$args['status'] = $status;
|
||||
}
|
||||
|
||||
$search = $request->get_param('search');
|
||||
if ($search !== '') {
|
||||
$args['search'] = $search;
|
||||
}
|
||||
|
||||
$query = new \WP_Comment_Query($args);
|
||||
|
||||
// Batch prefetch to avoid N+1 queries
|
||||
$user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
|
||||
$post_ids = array_unique(array_map(fn($c) => (int) $c->comment_post_ID, $query->comments));
|
||||
$users_map = [];
|
||||
$posts_map = [];
|
||||
if (!empty($user_ids)) {
|
||||
$users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
|
||||
foreach ($users as $u) $users_map[$u->ID] = $u;
|
||||
}
|
||||
if (!empty($post_ids)) {
|
||||
global $wpdb;
|
||||
$ids_csv = implode(',', array_map('intval', $post_ids));
|
||||
$titles = $wpdb->get_results("SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ({$ids_csv})", OBJECT_K);
|
||||
foreach ($titles as $t) $posts_map[$t->ID] = $t->post_title;
|
||||
}
|
||||
|
||||
$comments = [];
|
||||
foreach ($query->comments as $comment) {
|
||||
$comments[] = $this->format_comment($comment, $users_map, $posts_map);
|
||||
}
|
||||
|
||||
// Total count for pagination
|
||||
$count_args = $args;
|
||||
unset($count_args['number'], $count_args['offset'], $count_args['orderby'], $count_args['order']);
|
||||
$count_args['count'] = true;
|
||||
$total = (int) (new \WP_Comment_Query($count_args))->get_comments('count');
|
||||
|
||||
return $this->success([
|
||||
'data' => $comments,
|
||||
'total' => $total,
|
||||
'pages' => ceil($total / (int) $request->get_param('per_page')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /comments — Create a comment.
|
||||
*/
|
||||
public function create_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$content = trim((string) $request->get_param('content'));
|
||||
$parent = (int) $request->get_param('parent');
|
||||
|
||||
if (!$content) {
|
||||
return $this->error('content_required', '评论内容不能为空。', 400);
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_status !== 'publish') {
|
||||
return $this->error('invalid_post', '文章不存在或未发布。', 400);
|
||||
}
|
||||
|
||||
$comment_data = [
|
||||
'comment_post_ID' => $post_id,
|
||||
'comment_content' => $content,
|
||||
'user_id' => $user_id,
|
||||
'comment_parent' => $parent,
|
||||
'comment_approved'=> 1, // Admin-created comments are auto-approved
|
||||
];
|
||||
|
||||
$comment_id = wp_insert_comment($comment_data);
|
||||
if (is_wp_error($comment_id)) {
|
||||
return $this->error('create_failed', $comment_id->get_error_message(), 500);
|
||||
}
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
return $this->success(['data' => $this->format_comment($comment)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /comments/{id}/status — Approve/unapprove/spam/trash.
|
||||
*/
|
||||
public function update_comment_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = (int) $request->get_param('id');
|
||||
$new_status = $request->get_param('status');
|
||||
$comment = get_comment($comment_id);
|
||||
|
||||
if (!$comment) {
|
||||
return $this->error('not_found', '评论不存在。', 404);
|
||||
}
|
||||
|
||||
$valid_statuses = ['approve', 'hold', 'spam', 'trash'];
|
||||
if (!in_array($new_status, $valid_statuses, true)) {
|
||||
return $this->error('invalid_status', '无效的状态值。', 400);
|
||||
}
|
||||
|
||||
$result = wp_set_comment_status($comment_id, $new_status);
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error('update_failed', $result->get_error_message(), 500);
|
||||
}
|
||||
|
||||
// Handle spam/trash
|
||||
if ($new_status === 'spam') {
|
||||
wp_spam_comment($comment_id);
|
||||
} elseif ($new_status === 'trash') {
|
||||
wp_trash_comment($comment_id);
|
||||
}
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
return $this->success(['data' => $this->format_comment($comment)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /comments/{id} — Edit comment content.
|
||||
*/
|
||||
public function update_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = (int) $request->get_param('id');
|
||||
$content = trim((string) $request->get_param('content'));
|
||||
$comment = get_comment($comment_id);
|
||||
|
||||
if (!$comment) {
|
||||
return $this->error('not_found', '评论不存在。', 404);
|
||||
}
|
||||
|
||||
if (!$content) {
|
||||
return $this->error('content_required', '评论内容不能为空。', 400);
|
||||
}
|
||||
|
||||
$result = wp_update_comment(['comment_ID' => $comment_id, 'comment_content' => $content]);
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error('update_failed', $result->get_error_message(), 500);
|
||||
}
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
return $this->success(['data' => $this->format_comment($comment)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /comments/{id} — Delete a comment.
|
||||
*/
|
||||
public function delete_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = (int) $request->get_param('id');
|
||||
$comment = get_comment($comment_id);
|
||||
|
||||
if (!$comment) {
|
||||
return $this->error('not_found', '评论不存在。', 404);
|
||||
}
|
||||
|
||||
$result = wp_delete_comment($comment_id, true);
|
||||
if (!$result) {
|
||||
return $this->error('delete_failed', '删除评论失败。', 500);
|
||||
}
|
||||
|
||||
return $this->success(['data' => ['deleted' => true, 'id' => $comment_id]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /comments/bulk — Bulk action on comments.
|
||||
*/
|
||||
public function bulk_action(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$body = $request->get_json_params();
|
||||
$ids = array_map('absint', $body['ids'] ?? []);
|
||||
$action = sanitize_text_field($body['action'] ?? '');
|
||||
|
||||
if (empty($ids) || !$action) {
|
||||
return $this->error('missing_params', '缺少参数。', 400);
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$comment = get_comment($id);
|
||||
if (!$comment) { $failed++; continue; }
|
||||
|
||||
switch ($action) {
|
||||
case 'approve':
|
||||
wp_set_comment_status($id, 'approve');
|
||||
$success++;
|
||||
break;
|
||||
case 'hold':
|
||||
wp_set_comment_status($id, 'hold');
|
||||
$success++;
|
||||
break;
|
||||
case 'spam':
|
||||
wp_spam_comment($id);
|
||||
$success++;
|
||||
break;
|
||||
case 'trash':
|
||||
wp_trash_comment($id);
|
||||
$success++;
|
||||
break;
|
||||
case 'delete':
|
||||
wp_delete_comment($id, true);
|
||||
$success++;
|
||||
break;
|
||||
default:
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['data' => ['success' => $success, 'failed' => $failed]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /comments/post/{post_id} — Public: list comments for a post with nesting.
|
||||
*/
|
||||
public function get_post_comments(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$post_id = (int) $request->get_param('post_id');
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(50, max(1, (int) $request->get_param('per_page')));
|
||||
$order = in_array(strtoupper((string) $request->get_param('order')), ['ASC', 'DESC'], true)
|
||||
? strtoupper((string) $request->get_param('order'))
|
||||
: 'DESC';
|
||||
|
||||
$args = [
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'number' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => $order,
|
||||
'hierarchical' => 'threaded',
|
||||
];
|
||||
|
||||
$query = new \WP_Comment_Query($args);
|
||||
|
||||
// Batch prefetch users
|
||||
$user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments)));
|
||||
$users_map = [];
|
||||
if (!empty($user_ids)) {
|
||||
$users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]);
|
||||
foreach ($users as $u) $users_map[$u->ID] = $u;
|
||||
}
|
||||
|
||||
$comments = [];
|
||||
foreach ($query->comments as $comment) {
|
||||
$comments[] = $this->format_comment_public($comment, $users_map);
|
||||
}
|
||||
|
||||
// Total approved comments for this post
|
||||
$total = (int) get_comments_number($post_id);
|
||||
$total_approved = get_comments([
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'count' => true,
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'data' => $comments,
|
||||
'total' => (int) $total_approved,
|
||||
'pages' => ceil((int) $total_approved / $per_page),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format comment for admin views.
|
||||
*/
|
||||
private function format_comment(\WP_Comment $comment, array $users_map = [], array $posts_map = []): array {
|
||||
$uid = (int) $comment->user_id;
|
||||
$pid = (int) $comment->comment_post_ID;
|
||||
$user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
|
||||
return [
|
||||
'id' => (int) $comment->comment_ID,
|
||||
'post_id' => $pid,
|
||||
'post_title' => $posts_map[$pid] ?? get_the_title($pid),
|
||||
'author' => $comment->comment_author,
|
||||
'author_email' => $comment->comment_author_email,
|
||||
'user_id' => (int) $comment->user_id,
|
||||
'user_name' => $user ? $user->display_name : '',
|
||||
'content' => $comment->comment_content,
|
||||
'status' => $comment->comment_approved,
|
||||
'parent' => (int) $comment->comment_parent,
|
||||
'date' => $comment->comment_date_gmt,
|
||||
'author_ip' => $comment->comment_author_IP,
|
||||
'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format comment for public views (no email/IP).
|
||||
*/
|
||||
private function format_comment_public(\WP_Comment $comment, array $users_map = []): array {
|
||||
$uid = (int) $comment->user_id;
|
||||
$user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null);
|
||||
return [
|
||||
'id' => (int) $comment->comment_ID,
|
||||
'author' => $comment->comment_author,
|
||||
'user_id' => (int) $comment->user_id,
|
||||
'content' => $comment->comment_content,
|
||||
'date' => $comment->comment_date_gmt,
|
||||
'parent' => (int) $comment->comment_parent,
|
||||
'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]),
|
||||
'children' => [], // Will be populated by JS if threaded
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: check logged-in permission (any authenticated user).
|
||||
*/
|
||||
public function check_logged_in_permission(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
abstract class ControllerBase {
|
||||
|
||||
/**
|
||||
* REST API namespace (shared with Admin).
|
||||
*/
|
||||
const API_NAMESPACE = 'aksrt/v1';
|
||||
|
||||
/**
|
||||
* Register REST routes for this domain.
|
||||
* Subclasses must implement this method.
|
||||
*/
|
||||
abstract public function register_routes(): void;
|
||||
|
||||
/**
|
||||
* Permission callback — requires manage_options capability.
|
||||
*/
|
||||
public function check_permission(): bool {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a standard success response.
|
||||
*/
|
||||
protected function success($data = [], int $status = 200): \WP_REST_Response {
|
||||
return new \WP_REST_Response(array_merge(['success' => true], $data), $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a standard error response.
|
||||
*/
|
||||
protected function error(string $message, int $status = 400): \WP_REST_Response {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON request body safely.
|
||||
*/
|
||||
protected function get_json_params(\WP_REST_Request $request): array {
|
||||
$params = $request->get_json_params();
|
||||
return is_array($params) ? $params : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a settings array using a whitelist of allowed keys.
|
||||
*
|
||||
* @param array $input Raw input data.
|
||||
* @param array $allowed Allowed keys with their types (string|int|bool|float|array).
|
||||
* @return array Sanitized data.
|
||||
*/
|
||||
protected function sanitize_fields(array $input, array $allowed): array {
|
||||
$out = [];
|
||||
foreach ($allowed as $key => $type) {
|
||||
if (!array_key_exists($key, $input)) continue;
|
||||
$val = $input[$key];
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
$out[$key] = sanitize_text_field($val);
|
||||
break;
|
||||
case 'textarea':
|
||||
$out[$key] = sanitize_textarea_field($val);
|
||||
break;
|
||||
case 'html':
|
||||
$out[$key] = wp_kses_post($val);
|
||||
break;
|
||||
case 'code':
|
||||
$out[$key] = is_string($val) ? wp_unslash($val) : '';
|
||||
break;
|
||||
case 'url':
|
||||
$out[$key] = esc_url_raw($val);
|
||||
break;
|
||||
case 'int':
|
||||
$out[$key] = (int) $val;
|
||||
break;
|
||||
case 'bool':
|
||||
$out[$key] = !empty($val);
|
||||
break;
|
||||
case 'float':
|
||||
$out[$key] = (float) $val;
|
||||
break;
|
||||
case 'array':
|
||||
$out[$key] = is_array($val) ? $val : [];
|
||||
break;
|
||||
default:
|
||||
$out[$key] = sanitize_text_field($val);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function register_section_routes(string $route_slug, string $domain): void {
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P<section>[a-z0-9_-]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => function (\WP_REST_Request $request) use ($domain) {
|
||||
return $this->get_section_settings($request, $domain);
|
||||
},
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/' . $route_slug . '/(?P<section>[a-z0-9_-]+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => function (\WP_REST_Request $request) use ($domain) {
|
||||
return $this->save_section_settings($request, $domain);
|
||||
},
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function get_section_settings(\WP_REST_Request $request, string $domain): \WP_REST_Response {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$section = (string) $request->get_param('section');
|
||||
$schema = $provider->get_section_schema($domain, $section);
|
||||
|
||||
if (!$schema) {
|
||||
return $this->error('未知的设置分组: ' . esc_html($section), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => $provider->read_settings($domain, $section),
|
||||
'schema' => $schema,
|
||||
'payload' => [
|
||||
'domain' => $provider->normalize_domain($domain),
|
||||
'section' => $provider->normalize_section($section),
|
||||
'version' => $schema['version'] ?? '1.0.0',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function save_section_settings(\WP_REST_Request $request, string $domain): \WP_REST_Response {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$section = (string) $request->get_param('section');
|
||||
$schema = $provider->get_section_schema($domain, $section);
|
||||
|
||||
if (!$schema) {
|
||||
return $this->error('未知的设置分组: ' . esc_html($section), 404);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$payload = $params['data'] ?? $params;
|
||||
if (!is_array($payload)) {
|
||||
return $this->error('无效的设置载荷');
|
||||
}
|
||||
|
||||
$sanitized = $this->sanitize_fields($payload, $provider->get_allowed_fields($domain, $section));
|
||||
$validation = $provider->validate($domain, $sanitized, $section);
|
||||
if (!$validation['valid']) {
|
||||
return $this->error(implode('; ', $validation['errors']), 422);
|
||||
}
|
||||
|
||||
$provider->write_settings($domain, $sanitized, $section);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf('%s/%s 设置已保存', $provider->normalize_domain($domain), $provider->normalize_section($section)),
|
||||
'data' => $provider->read_settings($domain, $section),
|
||||
'payload' => [
|
||||
'domain' => $provider->normalize_domain($domain),
|
||||
'section' => $provider->normalize_section($section),
|
||||
'version' => $schema['version'] ?? '1.0.0',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ExtensionController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
private $admin;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function set_admin(Admin $admin): void {
|
||||
$this->admin = $admin;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Extensions overview
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Extensions service status summary (aggregated)
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/extensions', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_extensions_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security settings summary
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/security', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security settings — save risk switches
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/security', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'save_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Security audit log
|
||||
register_rest_route(self::API_NAMESPACE, '/security/audit-log', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_audit_log'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// IP geo test tool
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/ip-test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_ip_geo'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Content audit test tool
|
||||
register_rest_route(self::API_NAMESPACE, '/extensions/audit-test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_content_audit'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /extensions/overview — real extension status aggregation.
|
||||
*/
|
||||
public function get_overview(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
|
||||
$installed = 0;
|
||||
$available = count($services);
|
||||
$unconfigured = 0;
|
||||
|
||||
foreach ($services as $svc) {
|
||||
if (!empty($svc['enabled'])) $installed++;
|
||||
if (!empty($svc['enabled']) && !empty($svc['configured']) && empty($svc['verified'])) {
|
||||
$unconfigured++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build message
|
||||
if ($installed === 0) {
|
||||
$message = sprintf(__('已注册 %d 项扩展,均未启用。', 'aksrt'), $available);
|
||||
} elseif ($unconfigured > 0) {
|
||||
$message = sprintf(__('已启用 %d/%d 项扩展,%d 项待补全配置。', 'aksrt'), $installed, $available, $unconfigured);
|
||||
} else {
|
||||
$message = sprintf(__('已启用 %d/%d 项扩展,全部就绪。', 'aksrt'), $installed, $available);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'installed' => $installed,
|
||||
'available' => $available,
|
||||
'message' => $message,
|
||||
'services' => $services,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_docs_stats(): \WP_REST_Response {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'total' => 0,
|
||||
'nav_count' => 0,
|
||||
'special_count' => 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /docs/search — search document pages by keyword.
|
||||
*/
|
||||
public function search_docs(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'results' => [],
|
||||
'total' => 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /settings/extensions — aggregated extension service status.
|
||||
*/
|
||||
public function get_extensions_status(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
$flat = [];
|
||||
|
||||
foreach ($services as $key => $svc) {
|
||||
$flat["{$key}_enabled"] = !empty($svc['enabled']);
|
||||
$flat["{$key}_configured"] = !empty($svc['configured']);
|
||||
$flat["{$key}_available"] = !empty($svc['available']);
|
||||
$flat["{$key}_verified"] = !empty($svc['verified']);
|
||||
$flat["{$key}_provider"] = (string) ($svc['provider'] ?? '');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => $flat,
|
||||
'services' => $services,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /settings/security — security settings summary with real service status.
|
||||
*/
|
||||
public function get_security_settings(): \WP_REST_Response {
|
||||
$services = $this->collect_service_statuses();
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
|
||||
$email_gate_enabled = true;
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
$email_gate_enabled = false;
|
||||
}
|
||||
|
||||
$captcha = function_exists('aksrt_get_captcha_status_summary')
|
||||
? aksrt_get_captcha_status_summary()
|
||||
: ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []];
|
||||
$features = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_cms_features() : [];
|
||||
$scenes = is_array($captcha['scenes'] ?? null) ? $captcha['scenes'] : [];
|
||||
|
||||
$risk_switches = [
|
||||
'show_unapproved_comments' => !empty($settings['show_unapproved_comments']),
|
||||
'skip_submit_moderation' => !empty($settings['skip_submit_moderation']),
|
||||
'guest_comment' => !empty($settings['guest_comment']),
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'email_gate_enabled' => $email_gate_enabled,
|
||||
'captcha' => [
|
||||
'enabled' => !empty($captcha['enabled']),
|
||||
'provider' => (string) ($captcha['provider'] ?? ''),
|
||||
'configured' => !empty($captcha['configured']),
|
||||
'scenes' => $scenes,
|
||||
'download_required' => !empty($features['download_captcha']),
|
||||
'download_active' => !empty($features['download_captcha']) && !empty($scenes['download']),
|
||||
],
|
||||
'content_audit' => [
|
||||
'enabled' => !empty($services['audit']['enabled']),
|
||||
'configured' => !empty($services['audit']['configured']),
|
||||
],
|
||||
'ip_geo' => [
|
||||
'enabled' => !empty($services['ip_geo']['enabled']),
|
||||
'configured' => !empty($services['ip_geo']['configured']),
|
||||
],
|
||||
'risk_switches' => $risk_switches,
|
||||
'services' => $services,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /settings/security — save risk switches.
|
||||
*/
|
||||
public function save_security_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$risk_switches = $params['risk_switches'] ?? [];
|
||||
|
||||
if (!is_array($risk_switches)) {
|
||||
return $this->error('无效的风险设置数据');
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'show_unapproved_comments' => 'bool',
|
||||
'skip_submit_moderation' => 'bool',
|
||||
'guest_comment' => 'bool',
|
||||
];
|
||||
|
||||
$sanitized = $this->sanitize_fields($risk_switches, $allowed);
|
||||
|
||||
// Merge into main settings
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
foreach ($sanitized as $key => $val) {
|
||||
$all[$key] = $val;
|
||||
}
|
||||
|
||||
update_option('aksrt_settings', $all);
|
||||
|
||||
return $this->success([
|
||||
'message' => __('风险设置已保存', 'aksrt'),
|
||||
'data' => $sanitized,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /security/audit-log — recent security audit entries.
|
||||
*/
|
||||
public function get_audit_log(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$limit = 20;
|
||||
$logs = [];
|
||||
|
||||
// Recent user registrations
|
||||
$recent_users = get_users([
|
||||
'number' => $limit,
|
||||
'orderby' => 'registered',
|
||||
'order' => 'DESC',
|
||||
'fields' => ['ID', 'display_name', 'user_email', 'user_registered'],
|
||||
]);
|
||||
|
||||
foreach ($recent_users as $user) {
|
||||
$logs[] = [
|
||||
'id' => 'reg_' . $user->ID,
|
||||
'action' => '用户注册',
|
||||
'detail' => sprintf('%s (%s) 完成注册', $user->display_name, $user->user_email),
|
||||
'time' => $user->user_registered,
|
||||
'type' => 'auth',
|
||||
];
|
||||
}
|
||||
|
||||
// Content audit log
|
||||
$audit_log = get_option('aksrt_content_audit_log', []);
|
||||
foreach (array_slice(array_reverse($audit_log), 0, 10) as $i => $entry) {
|
||||
$logs[] = [
|
||||
'id' => 'audit_' . $i,
|
||||
'action' => '内容审核',
|
||||
'detail' => sprintf('%s — %s', $entry['content'] ?? '', ($entry['passed'] ?? false) ? '通过' : '拦截'),
|
||||
'time' => $entry['time'] ?? '',
|
||||
'type' => 'audit',
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by time descending
|
||||
usort($logs, function ($a, $b) {
|
||||
return strcmp($b['time'] ?? '', $a['time'] ?? '');
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'logs' => array_slice($logs, 0, $limit),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /extensions/ip-test — test current IP provider with a given IP.
|
||||
*
|
||||
* Admin tests force a live lookup (skip enable-gate + cache) so operators can
|
||||
* verify credentials before flipping the runtime switch.
|
||||
*/
|
||||
public function test_ip_geo(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$ip = trim((string) ($params['ip'] ?? ''));
|
||||
|
||||
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $this->error(__('请输入有效的 IP 地址', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$geo = \AKSRT\Extensions\IPGeo::get_instance();
|
||||
$status = $geo->get_status();
|
||||
$result = $geo->lookup($ip, true);
|
||||
|
||||
$reason = (string) ($result['reason'] ?? '');
|
||||
$message = '';
|
||||
$ok = !empty($result['country']) || !empty($result['province']) || !empty($result['city']);
|
||||
|
||||
// Persist "verified" once a live lookup succeeds so overview cards leave "待补全".
|
||||
if ($ok) {
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
if (empty($all['ip_geo_verified'])) {
|
||||
$all['ip_geo_verified'] = true;
|
||||
update_option('aksrt_settings', $all);
|
||||
if (method_exists('\AKSRT\Settings\Settings', 'flush_settings_cache')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
$status = $geo->get_status();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
$detail = trim((string) ($result['detail'] ?? ''));
|
||||
switch ($reason) {
|
||||
case 'private_ip':
|
||||
$message = __('该地址为私有/保留 IP(如 127.0.0.1 / 内网),无法查询。请用公网 IP 测试,例如 114.114.114.114。', 'aksrt');
|
||||
break;
|
||||
case 'invalid_ip':
|
||||
$message = __('IP 格式无效。', 'aksrt');
|
||||
break;
|
||||
case 'missing_key':
|
||||
$message = __('当前服务商 AppKey 未填写,请在「扩展 → IP归属地」补全凭据。', 'aksrt');
|
||||
break;
|
||||
case 'provider_error':
|
||||
// e.g. USERKEY_PLAT_NOMATCH / INVALID_USER_KEY from Amap
|
||||
if (stripos($detail, 'USERKEY_PLAT_NOMATCH') !== false) {
|
||||
$message = __('高德返回 USERKEY_PLAT_NOMATCH:当前 Key 平台类型不匹配。请在高德开放平台新建「Web服务」类型 Key,并开通 IP 定位,不要用 Web端(JS)/Android/iOS Key。', 'aksrt');
|
||||
} elseif (stripos($detail, 'INVALID_USER_KEY') !== false) {
|
||||
$message = __('高德返回 INVALID_USER_KEY:AppKey 无效或未启用,请核对 Key 是否复制完整、是否已启用。', 'aksrt');
|
||||
} else {
|
||||
$message = $detail !== ''
|
||||
? sprintf(
|
||||
/* translators: %s: provider error code/message */
|
||||
__('服务商拒绝请求:%s。请检查 Key 权限(IP定位)与配额。', 'aksrt'),
|
||||
$detail
|
||||
)
|
||||
: __('服务商返回错误,请检查 Key 权限与配额。', 'aksrt');
|
||||
}
|
||||
break;
|
||||
case 'http_error':
|
||||
$message = $detail !== ''
|
||||
? sprintf(
|
||||
/* translators: %s: transport error */
|
||||
__('无法访问归属地接口:%s。请检查服务器出站网络/防火墙。', 'aksrt'),
|
||||
$detail
|
||||
)
|
||||
: __('无法访问归属地接口,请检查服务器出站网络。', 'aksrt');
|
||||
break;
|
||||
case 'provider_empty':
|
||||
if (!empty($status['missing_fields'])) {
|
||||
$message = sprintf(
|
||||
/* translators: %s: missing config field keys */
|
||||
__('当前服务商凭据不完整(缺少:%s)。', 'aksrt'),
|
||||
implode(', ', (array) $status['missing_fields'])
|
||||
);
|
||||
} else {
|
||||
$message = __('服务商未返回有效归属地,请换公网 IP 或检查 Key 权限。', 'aksrt');
|
||||
}
|
||||
break;
|
||||
case 'disabled':
|
||||
$message = __('IP 归属地功能未启用。', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
$message = $detail !== '' ? $detail : __('未查询到归属地信息。', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'ip' => $ip,
|
||||
'ok' => $ok,
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
'result' => $result,
|
||||
'display' => $ok ? $geo->format($result) : '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /extensions/audit-test — test text content against current audit provider.
|
||||
*
|
||||
* Admin tests force a live provider call (skip enable gates + failure allow policy)
|
||||
* so operators can verify credentials before flipping runtime switches.
|
||||
*/
|
||||
public function test_content_audit(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$text = trim((string) ($params['text'] ?? ''));
|
||||
|
||||
if ($text === '') {
|
||||
return $this->error(__('请输入要测试的文本内容', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Extensions\ContentAudit')) {
|
||||
return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$audit = \AKSRT\Extensions\ContentAudit::get_instance();
|
||||
$status = $audit->get_status();
|
||||
$result = $audit->audit_text($text, true);
|
||||
|
||||
$reason = (string) ($result['reason'] ?? '');
|
||||
$message = '';
|
||||
$ok = !empty($result['passed']);
|
||||
|
||||
// config/api 错误时 passed=false 且 reason 是机器码,需要可读提示
|
||||
$error_reasons = ['config_missing', 'api_error', 'file_error', 'disabled'];
|
||||
$is_error = in_array($reason, $error_reasons, true)
|
||||
|| (str_starts_with($reason, 'config_') && $reason !== '');
|
||||
|
||||
if ($is_error || (!$ok && $reason !== '')) {
|
||||
switch ($reason) {
|
||||
case 'config_missing':
|
||||
if (!empty($status['missing_fields'])) {
|
||||
$message = sprintf(
|
||||
/* translators: %s: missing config field keys */
|
||||
__('当前审核服务商凭据不完整(缺少:%s),请先在「扩展 → 内容审核」补全配置。', 'aksrt'),
|
||||
implode(', ', (array) $status['missing_fields'])
|
||||
);
|
||||
} else {
|
||||
$message = __('审核服务商凭据缺失,请先补全 API Key / Secret。', 'aksrt');
|
||||
}
|
||||
$ok = false;
|
||||
break;
|
||||
case 'api_error':
|
||||
$message = __('调用审核接口失败,请检查凭据、网络出口或服务商状态。', 'aksrt');
|
||||
$ok = false;
|
||||
break;
|
||||
case 'disabled':
|
||||
$message = __('内容审核功能未启用。', 'aksrt');
|
||||
$ok = false;
|
||||
break;
|
||||
case 'non_compliant':
|
||||
$message = __('审核结论:不合规(已拦截)。', 'aksrt');
|
||||
break;
|
||||
case 'suspected':
|
||||
$message = __('审核结论:疑似违规。', 'aksrt');
|
||||
break;
|
||||
case 'review_failed':
|
||||
$message = __('审核结论:服务商审核失败。', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
if (!$ok) {
|
||||
$message = $reason !== ''
|
||||
? sprintf(/* translators: %s: provider reason */ __('审核未通过:%s', 'aksrt'), $reason)
|
||||
: __('审核未通过。', 'aksrt');
|
||||
}
|
||||
}
|
||||
} elseif ($ok) {
|
||||
if ($reason === 'passthrough' || $reason === '') {
|
||||
$message = __('审核通过。', 'aksrt');
|
||||
} else {
|
||||
$message = sprintf(/* translators: %s: provider reason */ __('审核通过(%s)。', 'aksrt'), $reason);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'text' => $text,
|
||||
'ok' => $ok,
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
'result' => $result,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有扩展服务的运行时状态
|
||||
*/
|
||||
private function collect_service_statuses(): array {
|
||||
$services = [];
|
||||
|
||||
// SMS
|
||||
if (class_exists('\AKSRT\Sms\SMS')) {
|
||||
$services['sms'] = \AKSRT\Sms\SMS::get_instance()->get_provider_status();
|
||||
} else {
|
||||
$services['sms'] = ['enabled' => false, 'provider' => 'none', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// WeChat
|
||||
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||
$services['wechat'] = \AKSRT\Payment\WeChatNotify::get_instance()->get_status();
|
||||
} else {
|
||||
$services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Captcha — 唯一真相源 aksrt_captcha_config
|
||||
$captcha = function_exists('aksrt_get_captcha_status_summary')
|
||||
? aksrt_get_captcha_status_summary()
|
||||
: ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []];
|
||||
$services['captcha'] = [
|
||||
'enabled' => !empty($captcha['enabled']) && !empty($captcha['configured']),
|
||||
'provider' => (string) ($captcha['provider'] ?? ''),
|
||||
'configured' => !empty($captcha['configured']),
|
||||
'available' => !empty($captcha['configured']),
|
||||
'verified' => false,
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => [],
|
||||
'scenes' => $captcha['scenes'] ?? [],
|
||||
];
|
||||
|
||||
// IP Geo
|
||||
if (class_exists('\AKSRT\Extensions\IPGeo')) {
|
||||
$services['ip_geo'] = \AKSRT\Extensions\IPGeo::get_instance()->get_status();
|
||||
} else {
|
||||
$services['ip_geo'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Content Audit
|
||||
if (class_exists('\AKSRT\Extensions\ContentAudit')) {
|
||||
$services['audit'] = \AKSRT\Extensions\ContentAudit::get_instance()->get_status();
|
||||
} else {
|
||||
$services['audit'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Search Push
|
||||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||||
$services['push'] = \AKSRT\Extensions\SearchPush::get_instance()->get_status();
|
||||
} else {
|
||||
$services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Docs Mode (compat path is retired from rebuilt mainline)
|
||||
$services['docs_mode'] = [
|
||||
'enabled' => false,
|
||||
'provider' => 'builtin',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
'verified' => true,
|
||||
'transport' => 'builtin',
|
||||
'missing_fields' => [],
|
||||
];
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class GlobalController extends ControllerBase {
|
||||
use GlobalControllerPart1;
|
||||
use GlobalControllerPart2;
|
||||
use HomepageSettingsRest;
|
||||
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all routes for the global domain.
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// Email / SMTP
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/mail', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_mail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/mail/test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_test_mail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Footer
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/footer', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_footer_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Footer Tabbar
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_footer_tabbar'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/footer-tabbar/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reorder_footer_tabbar'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Floating Buttons
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_floating_buttons'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/floating-buttons/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reorder_floating_buttons'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Friend Links
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_friend_links'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_friend_link'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/friend-links/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reorder_friend_links'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// CMS Features
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/cms-features', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_cms_features'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Homepage settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/homepage', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_homepage_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Category recommend
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/category-recommend', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_category_recommend'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Categories list (for admin selectors)
|
||||
register_rest_route(self::API_NAMESPACE, '/categories', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_categories'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Category term meta
|
||||
register_rest_route(self::API_NAMESPACE, '/categories/(?P<id>\d+)/meta', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_update_category_meta'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Homepage tab posts (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/homepage/tab-posts', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_homepage_tab_posts'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'term_id' => ['type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint'],
|
||||
'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'],
|
||||
'sort' => ['type' => 'string', 'default' => 'modified', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'view_mode' => ['type' => 'string', 'default' => 'list', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'page_url' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'esc_url_raw'],
|
||||
'panel' => ['type' => 'string', 'default' => 'latest', 'sanitize_callback' => 'sanitize_key'],
|
||||
'source' => ['type' => 'string', 'default' => 'home', 'sanitize_callback' => 'sanitize_key'],
|
||||
'tab_orderby' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Archive AJAX filter (public) — §2.5/§2.6/§2.7
|
||||
register_rest_route(self::API_NAMESPACE, '/archive/posts', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_archive_posts'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'archive_type' => ['type' => 'string', 'default' => 'category', 'sanitize_callback' => 'sanitize_key'],
|
||||
'term_id' => ['type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'filter_cat' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'filter_tag' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'sort' => ['type' => 'string', 'default' => 'date', 'sanitize_callback' => 'sanitize_key'],
|
||||
'order' => ['type' => 'string', 'default' => 'DESC', 'sanitize_callback' => 'sanitize_key'],
|
||||
'page' => ['type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint'],
|
||||
'custom_filters' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Homepage categories (for admin tab editor)
|
||||
register_rest_route(self::API_NAMESPACE, '/homepage/categories', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_homepage_categories'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// User search (for admin user selectors)
|
||||
register_rest_route(self::API_NAMESPACE, '/users/search', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_search_users'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'search' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'number' => ['type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_handle_cms_features(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
return $this->success(['cms_features' => \AKSRT\Settings\Settings::get_cms_features()]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$allowed = [
|
||||
'resource_cpt_enabled' => 'bool',
|
||||
'download_login' => 'bool',
|
||||
'download_credit' => 'bool',
|
||||
'download_captcha' => 'bool',
|
||||
'search_include_resource' => 'bool',
|
||||
];
|
||||
$sanitized = $this->sanitize_fields($params, $allowed);
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$settings['cms_features'] = array_merge(
|
||||
is_array($settings['cms_features'] ?? null) ? $settings['cms_features'] : [],
|
||||
$sanitized
|
||||
);
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => 'CMS 功能已保存', 'cms_features' => \AKSRT\Settings\Settings::get_cms_features()]);
|
||||
}
|
||||
|
||||
public function rest_handle_mail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$option_key = 'aksrt_mail_config';
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$config = get_option($option_key, [
|
||||
'smtp_host' => '',
|
||||
'smtp_port' => 465,
|
||||
'smtp_user' => '',
|
||||
'smtp_pass' => '',
|
||||
'smtp_secure' => 'ssl',
|
||||
'from_email' => '',
|
||||
'from_name' => '',
|
||||
]);
|
||||
return $this->success(['data' => $config, 'code' => 'success']);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$current = get_option($option_key, []);
|
||||
$merged = array_merge(is_array($current) ? $current : [], $params);
|
||||
update_option($option_key, $merged);
|
||||
|
||||
return $this->success(['message' => '邮件配置已保存', 'code' => 'success']);
|
||||
}
|
||||
|
||||
public function rest_test_mail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$to = sanitize_email($params['to'] ?? '');
|
||||
if (!is_email($to)) {
|
||||
return $this->error(__('请输入有效的邮箱地址', 'aksrt'));
|
||||
}
|
||||
|
||||
$sent = wp_mail($to, __('AKSRT 邮件测试', 'aksrt'), __('如果你收到这封邮件,说明 SMTP 配置正确。', 'aksrt'));
|
||||
if ($sent) {
|
||||
return $this->success(['message' => '测试邮件已发送']);
|
||||
}
|
||||
return $this->error(__('邮件发送失败,请检查 SMTP 配置', 'aksrt'));
|
||||
}
|
||||
|
||||
public function rest_homepage_tab_posts(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$term_id = (int) $request->get_param('term_id');
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(50, max(1, (int) $request->get_param('per_page')));
|
||||
$sort = sanitize_key($request->get_param('sort') ?: 'modified');
|
||||
$view_mode = sanitize_key($request->get_param('view_mode') ?: 'list');
|
||||
$tab_orderby = sanitize_key($request->get_param('tab_orderby') ?: '');
|
||||
$panel = sanitize_key($request->get_param('panel') ?: 'latest');
|
||||
|
||||
$post_types = function_exists('aksrt_homepage_get_post_types')
|
||||
? aksrt_homepage_get_post_types()
|
||||
: ['post'];
|
||||
|
||||
$args = [
|
||||
'post_type' => $post_types,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $page,
|
||||
'ignore_sticky_posts' => $term_id > 0,
|
||||
];
|
||||
|
||||
if ($term_id > 0) {
|
||||
$term = get_term($term_id);
|
||||
$taxonomy = ($term && !is_wp_error($term)) ? (string) $term->taxonomy : 'category';
|
||||
if (!in_array($taxonomy, ['category', 'topics'], true)) {
|
||||
$taxonomy = 'category';
|
||||
}
|
||||
$args['tax_query'] = [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => [$term_id],
|
||||
]];
|
||||
}
|
||||
|
||||
$exclude_ids = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::home_exclude_post_ids()
|
||||
: [];
|
||||
if (!empty($exclude_ids)) {
|
||||
$args['post__not_in'] = $exclude_ids;
|
||||
}
|
||||
|
||||
if ($panel === 'latest' || $term_id <= 0) {
|
||||
$exclude_cat_ids = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::home_exclude_category_ids()
|
||||
: [];
|
||||
if (!empty($exclude_cat_ids)) {
|
||||
$args['category__not_in'] = $exclude_cat_ids;
|
||||
}
|
||||
}
|
||||
|
||||
$effective_sort = $tab_orderby !== '' ? $tab_orderby : $sort;
|
||||
if (function_exists('aksrt_homepage_apply_named_sort_args')) {
|
||||
$args = aksrt_homepage_apply_named_sort_args($args, $effective_sort);
|
||||
} else {
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
|
||||
$mode = in_array($view_mode, ['card', 'list'], true) ? $view_mode : '';
|
||||
$html = '';
|
||||
if (function_exists('aksrt_render_post_list')) {
|
||||
$overrides = ['context' => 'home'];
|
||||
if ($mode !== '') {
|
||||
$overrides['mode'] = $mode;
|
||||
}
|
||||
$render_config = function_exists('aksrt_resolve_post_list_render_config')
|
||||
? aksrt_resolve_post_list_render_config([
|
||||
'context' => 'home',
|
||||
'overrides' => $overrides,
|
||||
])
|
||||
: array_merge(['mode' => ($mode !== '' ? $mode : 'list'), 'context' => 'home'], $overrides);
|
||||
$render_config['skip_wrapper'] = true;
|
||||
$html = aksrt_render_post_list($query, $render_config, false);
|
||||
} elseif ($query->have_posts()) {
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
if (function_exists('aksrt_homepage_render_post_card')) {
|
||||
ob_start();
|
||||
aksrt_homepage_render_post_card();
|
||||
$html .= ob_get_clean();
|
||||
}
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$has_more = $page < (int) $query->max_num_pages;
|
||||
|
||||
return $this->success([
|
||||
'html' => $html,
|
||||
'total' => (int) $query->found_posts,
|
||||
'has_more' => $has_more,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_archive_posts(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->rest_homepage_tab_posts($request);
|
||||
}
|
||||
|
||||
public function rest_homepage_categories(): \WP_REST_Response {
|
||||
$data = [];
|
||||
|
||||
$cats = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
|
||||
foreach ($cats as $cat) {
|
||||
$data[] = [
|
||||
'id' => (int) $cat->term_id,
|
||||
'term_id' => (int) $cat->term_id,
|
||||
'name' => $cat->name,
|
||||
'slug' => $cat->slug,
|
||||
'count' => (int) $cat->count,
|
||||
'parent' => (int) $cat->parent,
|
||||
'taxonomy' => 'category',
|
||||
];
|
||||
}
|
||||
|
||||
if (taxonomy_exists('topics')) {
|
||||
$topics = get_terms([
|
||||
'taxonomy' => 'topics',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
]);
|
||||
if (!is_wp_error($topics)) {
|
||||
foreach ($topics as $term) {
|
||||
$data[] = [
|
||||
'id' => (int) $term->term_id,
|
||||
'term_id' => (int) $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'count' => (int) $term->count,
|
||||
'parent' => (int) $term->parent,
|
||||
'taxonomy' => 'topics',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_handle_friend_links(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$links = aksrt_get_friend_links();
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $links]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$links = get_option('aksrt_friend_links', []);
|
||||
$new_link = [
|
||||
'id' => uniqid('fl_'),
|
||||
'name' => sanitize_text_field($params['name'] ?? ''),
|
||||
'url' => esc_url_raw($params['url'] ?? ''),
|
||||
'logo' => esc_url_raw($params['logo'] ?? ''),
|
||||
'description' => sanitize_textarea_field($params['description'] ?? ''),
|
||||
'sort_order' => (int) ($params['sort_order'] ?? 0),
|
||||
'active' => 1,
|
||||
];
|
||||
$links[] = $new_link;
|
||||
usort($links, function ($a, $b) {
|
||||
return ($a['sort_order'] ?? 0) <=> ($b['sort_order'] ?? 0);
|
||||
});
|
||||
update_option('aksrt_friend_links', $links);
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $new_link, 'message' => __('Friend link added.', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_delete_friend_link(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = $request->get_param('id');
|
||||
$links = get_option('aksrt_friend_links', []);
|
||||
$links = array_filter($links, function ($link) use ($id) {
|
||||
return ($link['id'] ?? '') !== $id;
|
||||
});
|
||||
$links = array_values($links);
|
||||
update_option('aksrt_friend_links', $links);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Friend link deleted.', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_reorder_friend_links(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$ordered_ids = $request->get_param('ids');
|
||||
if (!is_array($ordered_ids)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
|
||||
}
|
||||
$links = get_option('aksrt_friend_links', []);
|
||||
$order_map = array_flip($ordered_ids);
|
||||
usort($links, function ($a, $b) use ($order_map) {
|
||||
$idx_a = $order_map[($a['id'] ?? '')] ?? 999;
|
||||
$idx_b = $order_map[($b['id'] ?? '')] ?? 999;
|
||||
return $idx_a <=> $idx_b;
|
||||
});
|
||||
foreach ($links as $index => &$link) {
|
||||
$link['sort_order'] = $index;
|
||||
}
|
||||
update_option('aksrt_friend_links', $links);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_footer_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$footer = get_option('aksrt_footer_settings', []);
|
||||
$defaults = [
|
||||
'contacts' => [],
|
||||
'quick_links' => [],
|
||||
'more_links' => [],
|
||||
'hot_categories' => 5,
|
||||
'copyright_text' => '',
|
||||
'layout_template' => 'template_1',
|
||||
'footer_t1_m_s' => ['pc', 'mobile'],
|
||||
'footer_t1_img' => '',
|
||||
'footer_t1_img_dark' => '',
|
||||
'footer_t1_t' => '',
|
||||
'fcode_t1_code' => '',
|
||||
'footer_contact_m_s' => ['pc', 'mobile'],
|
||||
'footer_contact_wechat_img' => '',
|
||||
'footer_contact_qq' => '',
|
||||
'footer_contact_weibo' => '',
|
||||
'footer_contact_email' => '',
|
||||
'footer_contact_more' => '',
|
||||
'fcode_t2_code_1' => '',
|
||||
'fcode_t2_code_2' => '',
|
||||
'footer_mini_img_m_s' => ['pc', 'mobile'],
|
||||
'footer_mini_img' => [],
|
||||
'fcode_customize_code' => '',
|
||||
];
|
||||
$data = array_merge($defaults, $footer);
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $data]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$footer = [
|
||||
'contacts' => isset($params['contacts']) ? $this->sanitize_contacts($params['contacts']) : [],
|
||||
'quick_links' => isset($params['quick_links']) ? $this->sanitize_link_list($params['quick_links']) : [],
|
||||
'more_links' => isset($params['more_links']) ? $this->sanitize_link_list($params['more_links']) : [],
|
||||
'hot_categories' => (int) ($params['hot_categories'] ?? 5),
|
||||
'copyright_text' => sanitize_textarea_field($params['copyright_text'] ?? ''),
|
||||
'layout_template' => 'template_1',
|
||||
'footer_t1_m_s' => $this->sanitize_device_visibility($params['footer_t1_m_s'] ?? ['pc', 'mobile']),
|
||||
'footer_t1_img' => esc_url_raw($params['footer_t1_img'] ?? ''),
|
||||
'footer_t1_img_dark' => esc_url_raw($params['footer_t1_img_dark'] ?? ''),
|
||||
'footer_t1_t' => sanitize_text_field($params['footer_t1_t'] ?? ''),
|
||||
'fcode_t1_code' => wp_kses_post($params['fcode_t1_code'] ?? ''),
|
||||
'footer_contact_m_s' => $this->sanitize_device_visibility($params['footer_contact_m_s'] ?? ['pc', 'mobile']),
|
||||
'footer_contact_wechat_img' => esc_url_raw($params['footer_contact_wechat_img'] ?? ''),
|
||||
'footer_contact_qq' => sanitize_text_field($params['footer_contact_qq'] ?? ''),
|
||||
'footer_contact_weibo' => sanitize_text_field($params['footer_contact_weibo'] ?? ''),
|
||||
'footer_contact_email' => sanitize_email($params['footer_contact_email'] ?? ''),
|
||||
'footer_contact_more' => isset($params['footer_contact_more']) && is_array($params['footer_contact_more'])
|
||||
? $this->sanitize_contact_more($params['footer_contact_more']) : [],
|
||||
'fcode_t2_code_1' => wp_kses_post($params['fcode_t2_code_1'] ?? ''),
|
||||
'fcode_t2_code_2' => wp_kses_post($params['fcode_t2_code_2'] ?? ''),
|
||||
'footer_mini_img_m_s' => $this->sanitize_device_visibility($params['footer_mini_img_m_s'] ?? ['pc', 'mobile']),
|
||||
'footer_mini_img' => isset($params['footer_mini_img']) && is_array($params['footer_mini_img'])
|
||||
? $this->sanitize_mini_img_group($params['footer_mini_img']) : [],
|
||||
'fcode_customize_code' => wp_kses_post($params['fcode_customize_code'] ?? ''),
|
||||
];
|
||||
update_option('aksrt_footer_settings', $footer);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Footer settings saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
private function sanitize_contacts(array $contacts): array {
|
||||
$allowed_icons = ['github', 'x', 'twitter', 'email', 'wechat', 'qq', 'weibo',
|
||||
'telegram', 'discord', 'bilibili', 'linkedin', 'facebook',
|
||||
'youtube', 'instagram', 'rss', 'web', 'globe'];
|
||||
$clean = [];
|
||||
foreach ($contacts as $item) {
|
||||
$icon = sanitize_text_field($item['icon'] ?? '');
|
||||
if (!in_array($icon, $allowed_icons)) {
|
||||
continue;
|
||||
}
|
||||
$clean[] = [
|
||||
'icon' => $icon,
|
||||
'url' => esc_url_raw($item['url'] ?? ''),
|
||||
'title' => sanitize_text_field($item['title'] ?? ''),
|
||||
'enabled' => !empty($item['enabled']),
|
||||
];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function sanitize_link_list(array $links): array {
|
||||
$clean = [];
|
||||
foreach ($links as $item) {
|
||||
$clean[] = [
|
||||
'id' => isset($item['id']) ? sanitize_text_field($item['id']) : uniqid('link_'),
|
||||
'name' => sanitize_text_field($item['name'] ?? ''),
|
||||
'url' => esc_url_raw($item['url'] ?? '#'),
|
||||
'enabled' => !empty($item['enabled']),
|
||||
'sort_order' => (int) ($item['sort_order'] ?? 0),
|
||||
];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function sanitize_device_visibility($value): array {
|
||||
$arr = is_array($value) ? $value : [$value];
|
||||
$allowed = ['pc', 'mobile'];
|
||||
$clean = array_values(array_intersect(array_map('sanitize_key', $arr), $allowed));
|
||||
return $clean ?: ['pc', 'mobile'];
|
||||
}
|
||||
|
||||
private function sanitize_contact_more(array $items): array {
|
||||
$clean = [];
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$clean[] = [
|
||||
'desc' => sanitize_text_field($item['desc'] ?? ''),
|
||||
'icon' => sanitize_text_field($item['icon'] ?? ''),
|
||||
'link' => esc_url_raw($item['link'] ?? ''),
|
||||
'img' => esc_url_raw($item['img'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function sanitize_mini_img_group(array $items): array {
|
||||
$clean = [];
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
if (count($clean) >= 4) {
|
||||
break;
|
||||
}
|
||||
$clean[] = [
|
||||
'text' => sanitize_text_field($item['text'] ?? ''),
|
||||
'img' => esc_url_raw($item['img'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
public function rest_handle_footer_tabbar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_footer_tabbar_settings()
|
||||
: ['enabled' => true, 'scroll_hide' => false, 'scroll_down_hide' => true, 'items' => []];
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $settings]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$items = isset($params['items']) ? array_map([$this, 'sanitize_tabbar_item'], (array) $params['items']) : [];
|
||||
|
||||
$max_items = max(3, min(7, (int) ($params['max_items'] ?? 5)));
|
||||
if (count($items) > $max_items) {
|
||||
return new \WP_REST_Response([
|
||||
'code' => 'error',
|
||||
'message' => sprintf(__('Footer tabbar supports at most %d items.', 'aksrt'), $max_items),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$tabbar = [
|
||||
'enabled' => !empty($params['enabled']),
|
||||
'scroll_hide' => !empty($params['scroll_hide']),
|
||||
'scroll_down_hide' => !empty($params['scroll_down_hide']),
|
||||
'max_items' => $max_items,
|
||||
'icon_size' => max(16, min(32, (int) ($params['icon_size'] ?? 22))),
|
||||
'text_size' => max(8, min(14, (int) ($params['text_size'] ?? 10))),
|
||||
'show_text' => !empty($params['show_text']),
|
||||
'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '#f04494',
|
||||
'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '',
|
||||
'border' => !empty($params['border']),
|
||||
'shadow' => !empty($params['shadow']),
|
||||
'animation' => !empty($params['animation']),
|
||||
'double_click_home' => !empty($params['double_click_home']),
|
||||
'badge_dot' => !empty($params['badge_dot']),
|
||||
'items' => $items,
|
||||
];
|
||||
update_option('aksrt_footer_tabbar', $tabbar);
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Footer tabbar settings saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait GlobalControllerPart1 {
|
||||
|
||||
public function rest_reorder_footer_tabbar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$ordered_ids = $params['ids'] ?? [];
|
||||
|
||||
if (!is_array($ordered_ids)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
if (count($ordered_ids) > 5) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('Footer tabbar supports at most 5 items.', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
$tabbar = get_option('aksrt_footer_tabbar', []);
|
||||
$items = $tabbar['items'] ?? [];
|
||||
|
||||
$order_map = array_flip($ordered_ids);
|
||||
usort($items, function ($a, $b) use ($order_map) {
|
||||
$idx_a = $order_map[($a['id'] ?? '')] ?? 999;
|
||||
$idx_b = $order_map[($b['id'] ?? '')] ?? 999;
|
||||
return $idx_a <=> $idx_b;
|
||||
});
|
||||
|
||||
foreach ($items as $index => &$item) {
|
||||
$item['sort_order'] = $index;
|
||||
}
|
||||
|
||||
$tabbar['items'] = $items;
|
||||
update_option('aksrt_footer_tabbar', $tabbar);
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
private function sanitize_tabbar_item(array $item): array {
|
||||
$valid_types = ['home', 'category', 'link', 'user', 'auth', 'search', 'add'];
|
||||
$type = sanitize_text_field($item['type'] ?? 'link');
|
||||
if (!in_array($type, $valid_types, true)) {
|
||||
$type = 'link';
|
||||
}
|
||||
$badge_mode = sanitize_text_field($item['badge_mode'] ?? 'static');
|
||||
if (!in_array($badge_mode, ['static', 'dynamic'], true)) {
|
||||
$badge_mode = 'static';
|
||||
}
|
||||
|
||||
$add_items = [];
|
||||
if (!empty($item['add_items']) && is_array($item['add_items'])) {
|
||||
foreach ($item['add_items'] as $sub_item) {
|
||||
$label = sanitize_text_field($sub_item['label'] ?? '');
|
||||
$icon = sanitize_text_field($sub_item['icon'] ?? 'fas fa-pen');
|
||||
$url = esc_url_raw($sub_item['url'] ?? '');
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
$add_items[] = [
|
||||
'id' => sanitize_text_field($sub_item['id'] ?? uniqid('add_')),
|
||||
'label' => $label,
|
||||
'icon' => $icon,
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => sanitize_text_field($item['id'] ?? uniqid('tab_')),
|
||||
'type' => $type,
|
||||
'icon' => sanitize_text_field($item['icon'] ?? ''),
|
||||
'text' => sanitize_text_field($item['text'] ?? ''),
|
||||
'link' => esc_url_raw($item['link'] ?? ''),
|
||||
'badge' => sanitize_text_field($item['badge'] ?? ''),
|
||||
'badge_mode' => $badge_mode,
|
||||
'category_id' => absint($item['category_id'] ?? 0),
|
||||
'search_default_type' => (function() use ($item) {
|
||||
$v = $item['search_default_type'] ?? 'post';
|
||||
return in_array($v, ['post', 'category', 'tag'], true) ? $v : 'post';
|
||||
})(),
|
||||
'new_window' => !empty($item['new_window']),
|
||||
'add_items' => $add_items,
|
||||
'enabled' => !empty($item['enabled']),
|
||||
'sort_order' => (int) ($item['sort_order'] ?? 0),
|
||||
'ontop' => !empty($item['ontop']),
|
||||
];
|
||||
}
|
||||
|
||||
public function rest_handle_floating_buttons(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_floating_buttons_settings()
|
||||
: [
|
||||
'enabled' => true,
|
||||
'style' => 'round',
|
||||
'position' => 'bottom',
|
||||
'scroll_hide' => false,
|
||||
'scroll_down_hide' => true,
|
||||
'filter_blur' => [],
|
||||
'items' => [],
|
||||
];
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $settings]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$items = isset($params['items']) ? array_map([$this, 'sanitize_floating_button_item'], (array) $params['items']) : [];
|
||||
|
||||
$filter_blur = [];
|
||||
if (!empty($params['filter_blur']) && is_array($params['filter_blur'])) {
|
||||
foreach ($params['filter_blur'] as $device) {
|
||||
$device = sanitize_text_field((string) $device);
|
||||
if (in_array($device, ['pc', 'mobile'], true)) {
|
||||
$filter_blur[] = $device;
|
||||
}
|
||||
}
|
||||
$filter_blur = array_values(array_unique($filter_blur));
|
||||
}
|
||||
|
||||
$settings = [
|
||||
'enabled' => !empty($params['enabled']),
|
||||
'style' => in_array($params['style'] ?? '', ['round', 'square'], true) ? $params['style'] : 'round',
|
||||
'position' => in_array($params['position'] ?? '', ['bottom', 'center'], true) ? $params['position'] : 'bottom',
|
||||
'scroll_hide' => !empty($params['scroll_hide']),
|
||||
'scroll_down_hide' => !empty($params['scroll_down_hide']),
|
||||
'max' => max(1, min(10, (int) ($params['max'] ?? 5))),
|
||||
'size' => max(32, min(64, (int) ($params['size'] ?? 44))),
|
||||
'spacing' => max(4, min(24, (int) ($params['spacing'] ?? 10))),
|
||||
'shadow' => !empty($params['shadow']),
|
||||
'animation' => !empty($params['animation']),
|
||||
'opacity' => max(30, min(100, (int) ($params['opacity'] ?? 100))),
|
||||
'active_color' => sanitize_hex_color($params['active_color'] ?? '') ?: '',
|
||||
'bg_color' => sanitize_hex_color($params['bg_color'] ?? '') ?: '',
|
||||
'pc_show' => !empty($params['pc_show']),
|
||||
'm_show' => !empty($params['m_show']),
|
||||
'filter_blur' => $filter_blur,
|
||||
'items' => $items,
|
||||
];
|
||||
|
||||
update_option('aksrt_floating_buttons', $settings);
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Floating button settings saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_reorder_floating_buttons(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$ordered_ids = $params['ids'] ?? [];
|
||||
|
||||
if (!is_array($ordered_ids)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('参数错误', 'aksrt')], 400);
|
||||
}
|
||||
|
||||
$settings = get_option('aksrt_floating_buttons', []);
|
||||
$items = $settings['items'] ?? [];
|
||||
|
||||
$order_map = array_flip($ordered_ids);
|
||||
usort($items, function ($a, $b) use ($order_map) {
|
||||
$idx_a = $order_map[($a['id'] ?? '')] ?? 999;
|
||||
$idx_b = $order_map[($b['id'] ?? '')] ?? 999;
|
||||
return $idx_a <=> $idx_b;
|
||||
});
|
||||
|
||||
foreach ($items as $index => &$item) {
|
||||
$item['sort_order'] = $index;
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$settings['items'] = $items;
|
||||
update_option('aksrt_floating_buttons', $settings);
|
||||
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Order saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
private function sanitize_floating_button_item(array $item): array {
|
||||
$valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add'];
|
||||
$type = sanitize_text_field($item['type'] ?? 'custom');
|
||||
if (!in_array($type, $valid_types, true)) {
|
||||
$type = 'custom';
|
||||
}
|
||||
|
||||
$hover_width = (int) ($item['hover_width'] ?? 200);
|
||||
if ($hover_width < 120) {
|
||||
$hover_width = 120;
|
||||
}
|
||||
if ($hover_width > 480) {
|
||||
$hover_width = 480;
|
||||
}
|
||||
|
||||
$qq = preg_replace('/\D+/', '', (string) ($item['qq'] ?? ''));
|
||||
$action_mode = sanitize_text_field($item['action_mode'] ?? 'link');
|
||||
if (!in_array($action_mode, ['link', 'qrcode'], true)) {
|
||||
$action_mode = 'link';
|
||||
}
|
||||
$qr_size = (int) ($item['qr_size'] ?? 120);
|
||||
if ($qr_size < 80) {
|
||||
$qr_size = 80;
|
||||
}
|
||||
if ($qr_size > 240) {
|
||||
$qr_size = 240;
|
||||
}
|
||||
|
||||
$add_items = [];
|
||||
if (!empty($item['add_items']) && is_array($item['add_items'])) {
|
||||
foreach ($item['add_items'] as $add_item) {
|
||||
if (!is_array($add_item)) {
|
||||
continue;
|
||||
}
|
||||
$label = sanitize_text_field($add_item['label'] ?? '');
|
||||
$url = esc_url_raw($add_item['url'] ?? '');
|
||||
$icon_item = sanitize_text_field($add_item['icon'] ?? '');
|
||||
if ($label === '' && $url === '') {
|
||||
continue;
|
||||
}
|
||||
$add_items[] = [
|
||||
'id' => sanitize_text_field($add_item['id'] ?? uniqid('add_')),
|
||||
'label' => $label,
|
||||
'url' => $url,
|
||||
'icon' => $icon_item,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$icon = sanitize_text_field($item['icon'] ?? '');
|
||||
$icon_dark = sanitize_text_field($item['icon_dark'] ?? '');
|
||||
$icon_light = sanitize_text_field($item['icon_light'] ?? '');
|
||||
if ($type === 'theme_mode') {
|
||||
$icon = 'fas fa-sun';
|
||||
$icon_dark = '';
|
||||
$icon_light = '';
|
||||
}
|
||||
// VIP float always uses modal path; drop stale link/hover and normalize icons.
|
||||
$link = esc_url_raw($item['link'] ?? '');
|
||||
$hover = wp_kses_post($item['hover'] ?? '');
|
||||
if ($type === 'pay_vip') {
|
||||
$link = '';
|
||||
$hover = '';
|
||||
if ($icon === '' || str_contains($icon, 'fa-diamond') || $icon === 'fa fa-diamond') {
|
||||
$icon = 'fas fa-gem';
|
||||
}
|
||||
// Ensure FA5 prefix when bare fa-* is stored.
|
||||
if ($icon !== '' && !preg_match('/\b(fas|far|fab|fal|fad)\b/', $icon)) {
|
||||
$icon = 'fas ' . ltrim(preg_replace('/^fa\s+/', '', $icon) ?? $icon);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => sanitize_text_field($item['id'] ?? uniqid('float_')),
|
||||
'type' => $type,
|
||||
'icon' => $icon,
|
||||
'icon_dark' => $icon_dark,
|
||||
'icon_light' => $icon_light,
|
||||
'text' => sanitize_text_field($item['text'] ?? ''),
|
||||
'desc' => sanitize_text_field($item['desc'] ?? ''),
|
||||
'action_mode' => $action_mode,
|
||||
'qq' => $qq,
|
||||
'qrcode_img' => esc_url_raw($item['qrcode_img'] ?? ''),
|
||||
'wechat_img' => esc_url_raw($item['wechat_img'] ?? ''),
|
||||
'link' => $link,
|
||||
'add_items' => $add_items,
|
||||
'hover' => $hover,
|
||||
'qr_size' => $qr_size,
|
||||
'hover_width' => $hover_width,
|
||||
'pc_s' => !empty($item['pc_s']),
|
||||
'm_s' => !empty($item['m_s']),
|
||||
'color' => sanitize_text_field($item['color'] ?? ''),
|
||||
'bg' => sanitize_text_field($item['bg'] ?? ''),
|
||||
'enabled' => !empty($item['enabled']),
|
||||
'sort_order' => (int) ($item['sort_order'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait GlobalControllerPart2 {
|
||||
|
||||
public function rest_handle_category_recommend(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$settings = \AKSRT\Settings\Settings::get_category_recommend_settings();
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $settings]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$category_ids = array_map('absint', (array) ($params['category_recommend_category_ids'] ?? []));
|
||||
$category_ids = array_filter($category_ids, fn($id) => $id > 0);
|
||||
$category_ids = array_values(array_unique($category_ids));
|
||||
|
||||
$settings = [
|
||||
'enable_category_recommend' => !empty($params['enable_category_recommend']),
|
||||
'category_recommend_title' => sanitize_text_field($params['category_recommend_title'] ?? ''),
|
||||
'category_recommend_category_ids' => $category_ids,
|
||||
'category_recommend_posts_per_category' => max(1, min(10, (int) ($params['category_recommend_posts_per_category'] ?? 5))),
|
||||
'category_recommend_columns' => max(1, min(4, (int) ($params['category_recommend_columns'] ?? 2))),
|
||||
'category_recommend_show_description' => !empty($params['category_recommend_show_description']),
|
||||
'category_recommend_show_dates' => !empty($params['category_recommend_show_dates']),
|
||||
'category_recommend_show_more_link' => !empty($params['category_recommend_show_more_link']),
|
||||
];
|
||||
update_option('aksrt_category_recommend', $settings);
|
||||
\AKSRT\Settings\Settings::flush_category_recommend_cache();
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Category recommendation settings saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_get_categories(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$categories = get_categories([
|
||||
'orderby' => 'count',
|
||||
'order' => 'DESC',
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
$data = array_map(function ($cat) {
|
||||
return [
|
||||
'term_id' => (int) $cat->term_id,
|
||||
'name' => $cat->name,
|
||||
'slug' => $cat->slug,
|
||||
'count' => (int) $cat->count,
|
||||
'description' => $cat->description,
|
||||
'cover_id' => (int) get_term_meta($cat->term_id, 'aksrt_category_cover_id', true),
|
||||
'cover_url' => \AKSRT\Settings\Settings::get_category_cover_url((int) $cat->term_id),
|
||||
'recommend_desc' => (string) get_term_meta($cat->term_id, 'aksrt_category_recommend_desc', true),
|
||||
];
|
||||
}, $categories);
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => array_values($data)]);
|
||||
}
|
||||
|
||||
public function rest_search_users(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$search = (string) $request->get_param('search');
|
||||
$number = max(1, min(50, (int) $request->get_param('number')));
|
||||
|
||||
$args = [
|
||||
'number' => $number,
|
||||
'orderby' => 'display_name',
|
||||
'order' => 'ASC',
|
||||
'fields' => ['ID', 'display_name', 'user_login'],
|
||||
];
|
||||
if ($search !== '') {
|
||||
$args['search'] = '*' . $search . '*';
|
||||
$args['search_columns'] = ['display_name', 'user_login', 'user_email'];
|
||||
}
|
||||
|
||||
$users = get_users($args);
|
||||
$data = array_map(function ($user) {
|
||||
return [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'user_login' => $user->user_login,
|
||||
];
|
||||
}, $users);
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => array_values($data)]);
|
||||
}
|
||||
|
||||
public function rest_update_category_meta(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$term = get_term($id, 'category');
|
||||
if (!$term || is_wp_error($term)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => __('Category does not exist.', 'aksrt')], 404);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$cover_id = absint($params['cover_id'] ?? 0);
|
||||
$recommend_desc = sanitize_textarea_field($params['recommend_desc'] ?? '');
|
||||
|
||||
if ($cover_id > 0) {
|
||||
update_term_meta($id, 'aksrt_category_cover_id', $cover_id);
|
||||
} else {
|
||||
delete_term_meta($id, 'aksrt_category_cover_id');
|
||||
}
|
||||
|
||||
if ($recommend_desc !== '') {
|
||||
update_term_meta($id, 'aksrt_category_recommend_desc', $recommend_desc);
|
||||
} else {
|
||||
delete_term_meta($id, 'aksrt_category_recommend_desc');
|
||||
}
|
||||
|
||||
\AKSRT\Settings\Settings::flush_category_recommend_cache();
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('分类元数据已保存', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait HomepageSettingsRest {
|
||||
|
||||
public function rest_handle_homepage_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$option_key = 'aksrt_settings';
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
return rest_ensure_response([
|
||||
'code' => 'success',
|
||||
'data' => \AKSRT\Settings\HomeCms::get_homepage_fixed_settings(),
|
||||
]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
if (empty($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
if (!is_array($params)) {
|
||||
$params = [];
|
||||
}
|
||||
|
||||
$allowed_sorts = \AKSRT\Settings\HomeCms::homepage_allowed_sorts();
|
||||
$settings = [];
|
||||
|
||||
// --- Canonical Zibll main list contract ---
|
||||
if (array_key_exists('home_posts_list_s', $params)) {
|
||||
$settings['home_posts_list_s'] = !empty($params['home_posts_list_s']);
|
||||
}
|
||||
|
||||
if (array_key_exists('home_list1_orderby_s', $params)) {
|
||||
$settings['home_list1_orderby_s'] = !empty($params['home_list1_orderby_s']);
|
||||
}
|
||||
|
||||
if (isset($params['home_list1_orderby_option']) && is_array($params['home_list1_orderby_option'])) {
|
||||
$settings['home_list1_orderby_option'] = $this->sanitize_home_list1_orderby_option(
|
||||
$params['home_list1_orderby_option'],
|
||||
$allowed_sorts
|
||||
);
|
||||
}
|
||||
|
||||
if (array_key_exists('index_list_title', $params)) {
|
||||
$settings['index_list_title'] = sanitize_text_field((string) $params['index_list_title']);
|
||||
}
|
||||
|
||||
if (array_key_exists('home_exclude_posts', $params)) {
|
||||
$settings['home_exclude_posts'] = $this->sanitize_id_list($params['home_exclude_posts']);
|
||||
}
|
||||
|
||||
if (array_key_exists('home_exclude_cats', $params)) {
|
||||
$settings['home_exclude_cats'] = $this->sanitize_id_list($params['home_exclude_cats']);
|
||||
}
|
||||
|
||||
if (array_key_exists('home_lists', $params)) {
|
||||
$settings['home_lists'] = $this->sanitize_home_lists($params['home_lists'], $allowed_sorts);
|
||||
}
|
||||
|
||||
// --- AKSRT presentation extras (optional, not Zibll dual-write) ---
|
||||
if (isset($params['homepage_card_style'])) {
|
||||
$settings['homepage_card_style'] = in_array($params['homepage_card_style'], ['default', 'style2', 'style3'], true)
|
||||
? $params['homepage_card_style']
|
||||
: 'default';
|
||||
}
|
||||
if (array_key_exists('homepage_multi_thumb_enabled', $params)) {
|
||||
$settings['homepage_multi_thumb_enabled'] = !empty($params['homepage_multi_thumb_enabled']);
|
||||
}
|
||||
if (isset($params['homepage_multi_thumb_min_images'])) {
|
||||
$settings['homepage_multi_thumb_min_images'] = max(2, min(4, (int) $params['homepage_multi_thumb_min_images']));
|
||||
}
|
||||
if (array_key_exists('homepage_sidebar_mini_enabled', $params)) {
|
||||
$settings['homepage_sidebar_mini_enabled'] = !empty($params['homepage_sidebar_mini_enabled']);
|
||||
}
|
||||
if (isset($params['homepage_default_view'])) {
|
||||
$settings['homepage_default_view'] = in_array($params['homepage_default_view'], ['list', 'card'], true)
|
||||
? $params['homepage_default_view']
|
||||
: 'list';
|
||||
}
|
||||
if (array_key_exists('homepage_sort_ajax_enabled', $params)) {
|
||||
$settings['homepage_sort_ajax_enabled'] = !empty($params['homepage_sort_ajax_enabled']);
|
||||
}
|
||||
if (array_key_exists('homepage_sticky_tabs_enabled', $params)) {
|
||||
$settings['homepage_sticky_tabs_enabled'] = !empty($params['homepage_sticky_tabs_enabled']);
|
||||
}
|
||||
if (array_key_exists('home_list_sort_dropdown', $params)) {
|
||||
$settings['home_list_sort_dropdown'] = !empty($params['home_list_sort_dropdown']);
|
||||
}
|
||||
if (isset($params['homepage_tab_posts_per_page'])) {
|
||||
$settings['homepage_tab_posts_per_page'] = max(1, min(20, (int) $params['homepage_tab_posts_per_page']));
|
||||
}
|
||||
if (isset($params['homepage_tab_limit'])) {
|
||||
$settings['homepage_tab_limit'] = max(1, min(12, (int) $params['homepage_tab_limit']));
|
||||
}
|
||||
if (array_key_exists('homepage_module_hero_enabled', $params)) {
|
||||
$settings['homepage_module_hero_enabled'] = !empty($params['homepage_module_hero_enabled']);
|
||||
}
|
||||
if (array_key_exists('homepage_module_banner_enabled', $params)) {
|
||||
$settings['homepage_module_banner_enabled'] = !empty($params['homepage_module_banner_enabled']);
|
||||
}
|
||||
|
||||
$current = get_option($option_key, []);
|
||||
if (!is_array($current)) {
|
||||
$current = [];
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
$current[$key] = $value;
|
||||
}
|
||||
|
||||
// Canonical writes also remove superseded aliases from persisted settings.
|
||||
$superseded = [
|
||||
'home_posts_list_enabled',
|
||||
'home_list_sort_enabled',
|
||||
'home_list_sort_options',
|
||||
'home_list_title',
|
||||
'home_exclude_post_ids',
|
||||
'home_exclude_category_ids',
|
||||
'homepage_tabs',
|
||||
'homepage_default_sort',
|
||||
];
|
||||
foreach ($superseded as $dead_key) {
|
||||
unset($current[$dead_key]);
|
||||
}
|
||||
|
||||
update_option($option_key, $current);
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
update_option('aksrt_homepage_schema_version', gmdate('Y-m-d'));
|
||||
delete_transient('aksrt_homepage_cached_images');
|
||||
do_action('aksrt_homepage_settings_changed');
|
||||
|
||||
return rest_ensure_response([
|
||||
'code' => 'success',
|
||||
'message' => __('首页设置已保存', 'aksrt'),
|
||||
'data' => \AKSRT\Settings\HomeCms::get_homepage_fixed_settings(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $raw
|
||||
* @return int[]
|
||||
*/
|
||||
private function sanitize_id_list($raw): array {
|
||||
if (is_string($raw)) {
|
||||
$parts = preg_split('/[\s,]+/', $raw) ?: [];
|
||||
$raw = $parts;
|
||||
}
|
||||
if (!is_array($raw)) {
|
||||
return [];
|
||||
}
|
||||
return array_values(array_unique(array_filter(array_map('absint', $raw), static fn($id) => $id > 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $option
|
||||
* @param string[] $allowed_sorts
|
||||
* @return array{orderby:string,options:string[]}
|
||||
*/
|
||||
private function sanitize_home_list1_orderby_option(array $option, array $allowed_sorts): array {
|
||||
$orderby = sanitize_key((string) ($option['orderby'] ?? $option['default'] ?? 'modified'));
|
||||
if (!in_array($orderby, $allowed_sorts, true)) {
|
||||
$orderby = 'modified';
|
||||
}
|
||||
|
||||
$options = [];
|
||||
$raw_options = $option['options'] ?? $option['orderby_option'] ?? [];
|
||||
if (!is_array($raw_options)) {
|
||||
$raw_options = [];
|
||||
}
|
||||
foreach ($raw_options as $key) {
|
||||
$key = sanitize_key((string) $key);
|
||||
if (in_array($key, $allowed_sorts, true) && !in_array($key, $options, true)) {
|
||||
$options[] = $key;
|
||||
}
|
||||
}
|
||||
if ($options === []) {
|
||||
$options = ['modified', 'date', 'views', 'likes', 'comments'];
|
||||
}
|
||||
if (!in_array($orderby, $options, true)) {
|
||||
array_unshift($options, $orderby);
|
||||
}
|
||||
|
||||
return [
|
||||
'orderby' => $orderby,
|
||||
'options' => array_values($options),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $raw_tabs
|
||||
* @param string[] $allowed_sorts
|
||||
* @return array<int, array{term_id:int,title:string,orderby:string,enabled:bool}>
|
||||
*/
|
||||
private function sanitize_home_lists($raw_tabs, array $allowed_sorts): array {
|
||||
if (!is_array($raw_tabs)) {
|
||||
return [];
|
||||
}
|
||||
$tabs = [];
|
||||
foreach ($raw_tabs as $tab) {
|
||||
if (!is_array($tab)) {
|
||||
continue;
|
||||
}
|
||||
$term_id = absint($tab['term_id'] ?? 0);
|
||||
if ($term_id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$term = get_term($term_id);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
continue;
|
||||
}
|
||||
$taxonomy = (string) $term->taxonomy;
|
||||
if (!in_array($taxonomy, ['category', 'topics'], true)) {
|
||||
// Allow category always; topics if registered.
|
||||
if ($taxonomy !== 'category') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$orderby = sanitize_key((string) ($tab['orderby'] ?? ''));
|
||||
if ($orderby !== '' && !in_array($orderby, $allowed_sorts, true)) {
|
||||
$orderby = 'date';
|
||||
}
|
||||
$tabs[] = [
|
||||
'term_id' => $term_id,
|
||||
'title' => sanitize_text_field((string) ($tab['title'] ?? '')),
|
||||
'orderby' => $orderby,
|
||||
'enabled' => !array_key_exists('enabled', $tab) || !empty($tab['enabled']),
|
||||
];
|
||||
}
|
||||
return $tabs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ImportExportController extends ControllerBase {
|
||||
use ImportExportControllerPart1;
|
||||
use ImportExportControllerPart2;
|
||||
|
||||
|
||||
private static $instance = null;
|
||||
private const SCHEDULED_BACKUP_HOOK = 'aksrt_settings_scheduled_backup';
|
||||
private const MONTHLY_SCHEDULE = 'aksrt_monthly';
|
||||
|
||||
private function __construct() {
|
||||
add_filter('cron_schedules', [$this, 'register_backup_schedules']);
|
||||
add_action('init', [$this, 'sync_backup_schedule']);
|
||||
add_action(self::SCHEDULED_BACKUP_HOOK, [$this, 'run_scheduled_backup']);
|
||||
add_filter('upgrader_pre_install', [$this, 'handle_theme_upgrade_snapshot'], 10, 2);
|
||||
add_action('switch_theme', [$this, 'clear_backup_schedule']);
|
||||
}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST routes for import/export operations.
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// Export all settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/export', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_export_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Import settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/import', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_import_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Reset a domain to defaults
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/reset/(?P<domain>[a-z_]+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reset_domain'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Reset all schema-managed settings to defaults
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/reset-all', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_reset_all'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Health check / diagnostics
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/health', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_health_check'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Schema introspection (for frontend to discover available fields)
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P<domain>[a-z_]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_schema'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P<domain>[a-z_]+)/(?P<section>[a-z0-9_-]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_schema_section'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/schema/(?P<domain>[a-z_]+)/(?P<section>[a-z0-9_-]+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_save_schema_section'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Backup snapshots
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/backups', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'list_setting_backups'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/backups', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_setting_backup'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/backups/(?P<id>\d+)/restore', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'restore_setting_backup'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/backups/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_setting_backup'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Widget Template Management ──
|
||||
|
||||
// Export current widget configuration
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/export', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'export_widgets'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Import widget configuration
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/import', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'import_widgets'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// List saved widget templates
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/templates', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'list_widget_templates'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Save current layout as a named template
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/templates', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'save_widget_template'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Apply a saved template
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/templates/(?P<id>\d+)/apply', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'apply_widget_template'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Delete a saved template
|
||||
register_rest_route(self::API_NAMESPACE, '/widgets/templates/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_widget_template'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Handlers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Export all AKSRT settings as a JSON-serializable array.
|
||||
*/
|
||||
public function rest_export_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
$domains = $schema_provider->get_domain_keys();
|
||||
|
||||
$export = [
|
||||
'version' => AKSRT_VERSION ?? '3.4.0',
|
||||
'exported' => gmdate('Y-m-d H:i:s'),
|
||||
'domains' => [],
|
||||
];
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$export['domains'][$domain] = [
|
||||
'option_key' => $schema_provider->get_option_key($domain),
|
||||
'data' => $schema_provider->read_settings($domain),
|
||||
];
|
||||
}
|
||||
|
||||
// Also export standalone options from the registry
|
||||
$standalone_keys = \AKSRT\Settings\Settings::STANDALONE_OPTION_KEYS;
|
||||
|
||||
$export['standalone'] = [];
|
||||
foreach ($standalone_keys as $key) {
|
||||
$val = get_option($key);
|
||||
if ($val !== false) {
|
||||
$export['standalone'][$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$export = \AKSRT\Settings\Settings::redact_sensitive_settings($export);
|
||||
|
||||
return $this->success(['data' => $export]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings from a JSON payload.
|
||||
*
|
||||
* Request body: { domains: { domain: { data: {...} } }, standalone: { key: value } }
|
||||
* Optional: { dry_run: true } to validate without saving.
|
||||
*/
|
||||
public function rest_import_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$dry_run = !empty($params['dry_run']);
|
||||
|
||||
if (empty($params['domains']) && empty($params['standalone'])) {
|
||||
return $this->error('没有可导入的数据');
|
||||
}
|
||||
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
$results = [];
|
||||
|
||||
// Import domain settings
|
||||
if (!$dry_run) {
|
||||
$backup = $this->create_backup_record('manual', __('导入前自动快照', 'aksrt'));
|
||||
$this->persist_setting_backup($backup);
|
||||
}
|
||||
|
||||
// Import domain settings
|
||||
if (!empty($params['domains']) && is_array($params['domains'])) {
|
||||
foreach ($params['domains'] as $domain => $payload) {
|
||||
$data = $payload['data'] ?? $payload;
|
||||
if (!is_array($data)) {
|
||||
$results[$domain] = ['status' => 'error', 'message' => 'Invalid data format'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate
|
||||
$validation = $schema_provider->validate($domain, $data);
|
||||
if (!$validation['valid']) {
|
||||
$results[$domain] = ['status' => 'error', 'errors' => $validation['errors']];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dry_run) {
|
||||
$schema_provider->write_settings($domain, $data);
|
||||
$results[$domain] = ['status' => 'imported'];
|
||||
} else {
|
||||
$results[$domain] = ['status' => 'valid'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import standalone options. Secret-bearing options are never accepted from exports.
|
||||
$standalone = !empty($params['standalone']) && is_array($params['standalone'])
|
||||
? \AKSRT\Settings\Settings::filter_sensitive_import($params['standalone'])
|
||||
: [];
|
||||
if ($standalone !== []) {
|
||||
foreach ($standalone as $key => $value) {
|
||||
if (!preg_match('/^aksrt_/', $key)) {
|
||||
$results[$key] = ['status' => 'skipped', 'message' => 'Not an AKSRT option'];
|
||||
continue;
|
||||
}
|
||||
if (!$dry_run) {
|
||||
update_option($key, $value);
|
||||
$results[$key] = ['status' => 'imported'];
|
||||
} else {
|
||||
$results[$key] = ['status' => 'valid'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dry_run) {
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'dry_run' => $dry_run,
|
||||
'results' => $results,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a domain's settings to defaults.
|
||||
*/
|
||||
public function rest_reset_domain(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$domain = $request->get_param('domain');
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
|
||||
if (!in_array($domain, $schema_provider->get_domain_keys(), true)) {
|
||||
return $this->error('未知的设置域: ' . esc_html($domain), 404);
|
||||
}
|
||||
|
||||
$backup = $this->create_backup_record('reset_domain', sprintf(__('重置 %s 前快照', 'aksrt'), $domain));
|
||||
$this->persist_setting_backup($backup);
|
||||
|
||||
$schema_provider->reset_to_defaults($domain);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf('%s 域设置已重置为默认值', $domain),
|
||||
'data' => [
|
||||
'backup_id' => $backup['id'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_reset_all(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
$backup = $this->create_backup_record('reset_all', __('重置全部前快照', 'aksrt'));
|
||||
$this->persist_setting_backup($backup);
|
||||
|
||||
foreach ($schema_provider->get_domain_keys() as $domain) {
|
||||
$schema_provider->reset_to_defaults($domain);
|
||||
}
|
||||
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success([
|
||||
'message' => __('全部 schema 托管设置已重置为默认值', 'aksrt'),
|
||||
'data' => [
|
||||
'backup_id' => $backup['id'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check — returns system diagnostics.
|
||||
*/
|
||||
public function rest_health_check(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$checks = [];
|
||||
|
||||
// PHP version
|
||||
$checks['php_version'] = [
|
||||
'label' => 'PHP 版本',
|
||||
'value' => PHP_VERSION,
|
||||
'status' => version_compare(PHP_VERSION, '8.1', '>=') ? 'ok' : 'warning',
|
||||
];
|
||||
|
||||
// WordPress version
|
||||
$wp_version = get_bloginfo('version');
|
||||
$checks['wp_version'] = [
|
||||
'label' => 'WordPress 版本',
|
||||
'value' => $wp_version,
|
||||
'status' => version_compare($wp_version, '6.4', '>=') ? 'ok' : 'warning',
|
||||
];
|
||||
|
||||
// Theme version
|
||||
$theme = wp_get_theme();
|
||||
$checks['theme_version'] = [
|
||||
'label' => '主题版本',
|
||||
'value' => $theme->get('Version') ?? 'unknown',
|
||||
'status' => 'ok',
|
||||
];
|
||||
|
||||
// Database tables
|
||||
$expected_tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_invite',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
];
|
||||
$missing_tables = [];
|
||||
foreach ($expected_tables as $table) {
|
||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
|
||||
$missing_tables[] = str_replace($wpdb->prefix, '', $table);
|
||||
}
|
||||
}
|
||||
$checks['db_tables'] = [
|
||||
'label' => '数据表',
|
||||
'value' => empty($missing_tables) ? '全部就绪' : '缺少: ' . implode(', ', $missing_tables),
|
||||
'status' => empty($missing_tables) ? 'ok' : 'error',
|
||||
];
|
||||
|
||||
// Email verification backfill
|
||||
$backfill_done = get_option('aksrt_email_verification_backfill_done', false);
|
||||
$checks['email_backfill'] = [
|
||||
'label' => '邮箱验证迁移',
|
||||
'value' => $backfill_done ? '已完成' : '未完成',
|
||||
'status' => $backfill_done ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Option count
|
||||
$option_count = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
|
||||
);
|
||||
$checks['option_count'] = [
|
||||
'label' => 'AKSRT 选项数',
|
||||
'value' => $option_count,
|
||||
'status' => $option_count < 100 ? 'ok' : 'warning',
|
||||
];
|
||||
|
||||
return $this->success(['checks' => $checks]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait ImportExportControllerPart1 {
|
||||
|
||||
/**
|
||||
* Get the schema definition for a domain (for frontend introspection).
|
||||
*/
|
||||
public function rest_get_schema(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$domain = $request->get_param('domain');
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
|
||||
$schema = $schema_provider->get_schema($domain);
|
||||
if (!$schema) {
|
||||
return $this->error('未知的设置域: ' . esc_html($domain), 404);
|
||||
}
|
||||
|
||||
return $this->success(['schema' => $schema]);
|
||||
}
|
||||
|
||||
public function rest_get_schema_section(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->get_section_settings($request, (string) $request->get_param('domain'));
|
||||
}
|
||||
|
||||
public function rest_save_schema_section(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->save_section_settings($request, (string) $request->get_param('domain'));
|
||||
}
|
||||
|
||||
public function list_setting_backups(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$backups = get_option('aksrt_settings_backups', []);
|
||||
if (!is_array($backups)) {
|
||||
$backups = [];
|
||||
}
|
||||
|
||||
usort($backups, static function ($a, $b) {
|
||||
return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values($backups),
|
||||
'total' => count($backups),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_setting_backup(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'manual'));
|
||||
$label = sanitize_text_field((string) ($params['label'] ?? ''));
|
||||
|
||||
$allowed_types = ['manual', 'scheduled', 'reset_all', 'reset_domain', 'update_theme'];
|
||||
if (!in_array($type, $allowed_types, true)) {
|
||||
$type = 'manual';
|
||||
}
|
||||
|
||||
$backup = $this->build_settings_backup($type, $label);
|
||||
$this->persist_setting_backup($backup);
|
||||
|
||||
return $this->success([
|
||||
'message' => __('设置快照已创建', 'aksrt'),
|
||||
'data' => $backup,
|
||||
]);
|
||||
}
|
||||
|
||||
public function restore_setting_backup(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$backups = get_option('aksrt_settings_backups', []);
|
||||
if (!is_array($backups)) {
|
||||
$backups = [];
|
||||
}
|
||||
|
||||
$target = null;
|
||||
foreach ($backups as $backup) {
|
||||
if ((int) ($backup['id'] ?? 0) === $id) {
|
||||
$target = $backup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$target || !is_array($target['payload'] ?? null)) {
|
||||
return $this->error(__('备份快照不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$payload = $target['payload'];
|
||||
$domains = $payload['domains'] ?? [];
|
||||
$standalone = $payload['standalone'] ?? [];
|
||||
$provider = SchemaProvider::get_instance();
|
||||
|
||||
foreach ($domains as $domain => $domainPayload) {
|
||||
$data = $domainPayload['data'] ?? $domainPayload;
|
||||
if (is_array($data)) {
|
||||
$provider->write_settings((string) $domain, $data);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($standalone as $key => $value) {
|
||||
if (is_string($key) && preg_match('/^aksrt_/', $key)) {
|
||||
update_option($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success([
|
||||
'message' => __('设置已从快照恢复', 'aksrt'),
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete_setting_backup(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$backups = get_option('aksrt_settings_backups', []);
|
||||
if (!is_array($backups)) {
|
||||
$backups = [];
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
$found = false;
|
||||
foreach ($backups as $backup) {
|
||||
if ((int) ($backup['id'] ?? 0) === $id) {
|
||||
$found = true;
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $backup;
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
return $this->error(__('备份快照不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
update_option('aksrt_settings_backups', array_values($filtered));
|
||||
|
||||
return $this->success([
|
||||
'message' => __('备份快照已删除', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Widget Template Handlers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* GET /widgets/export — Export current widget configuration.
|
||||
*/
|
||||
public function export_widgets(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$sidebars_widgets = get_option('sidebars_widgets', []);
|
||||
$widget_options = $this->collect_widget_options();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'version' => AKSRT_VERSION ?? '3.4.0',
|
||||
'exported' => gmdate('Y-m-d H:i:s'),
|
||||
'sidebars_widgets' => $sidebars_widgets,
|
||||
'widget_options' => $widget_options,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /widgets/import — Import widget configuration.
|
||||
*/
|
||||
public function import_widgets(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $request->get_json_params() ?? [];
|
||||
$sidebars = $params['sidebars_widgets'] ?? null;
|
||||
$options = $params['widget_options'] ?? null;
|
||||
|
||||
if (!$sidebars || !is_array($sidebars)) {
|
||||
return $this->error('缺少 sidebars_widgets 数据');
|
||||
}
|
||||
|
||||
// Validate sidebar IDs exist
|
||||
$valid_sidebars = array_keys(get_registered_sidebars());
|
||||
$valid_sidebars[] = 'wp_inactive_widgets';
|
||||
|
||||
$imported = 0;
|
||||
foreach ($sidebars as $sidebar_id => $widget_ids) {
|
||||
if (!in_array($sidebar_id, $valid_sidebars, true) && $sidebar_id !== 'array_version') {
|
||||
continue;
|
||||
}
|
||||
$imported++;
|
||||
}
|
||||
|
||||
// Import widget options if provided
|
||||
if ($options && is_array($options)) {
|
||||
foreach ($options as $option_key => $option_value) {
|
||||
if (preg_match('/^widget_aksrt_/', $option_key)) {
|
||||
update_option($option_key, $option_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import sidebar assignments
|
||||
update_option('sidebars_widgets', $sidebars);
|
||||
|
||||
return $this->success(['message' => "已导入 {$imported} 个侧栏配置。"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /widgets/templates — List saved widget templates.
|
||||
*/
|
||||
public function list_widget_templates(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$templates = get_option('aksrt_widget_templates', []);
|
||||
return $this->success(['data' => ['templates' => $templates]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /widgets/templates — Save current layout as a named template.
|
||||
*/
|
||||
public function save_widget_template(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $request->get_json_params() ?? [];
|
||||
$name = sanitize_text_field((string) ($params['name'] ?? ''));
|
||||
if (!$name) {
|
||||
return $this->error('请输入模板名称');
|
||||
}
|
||||
|
||||
$templates = get_option('aksrt_widget_templates', []);
|
||||
$template = [
|
||||
'id' => count($templates) + 1,
|
||||
'name' => $name,
|
||||
'created_at' => gmdate('Y-m-d H:i:s'),
|
||||
'sidebars_widgets' => get_option('sidebars_widgets', []),
|
||||
'widget_options' => $this->collect_widget_options(),
|
||||
];
|
||||
|
||||
$templates[] = $template;
|
||||
update_option('aksrt_widget_templates', $templates);
|
||||
|
||||
return $this->success(['message' => '模板已保存', 'data' => $template]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /widgets/templates/{id}/apply — Apply a saved template.
|
||||
*/
|
||||
public function apply_widget_template(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$templates = get_option('aksrt_widget_templates', []);
|
||||
|
||||
$template = null;
|
||||
foreach ($templates as $t) {
|
||||
if ((int) ($t['id'] ?? 0) === $id) {
|
||||
$template = $t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
return $this->error('模板不存在', 404);
|
||||
}
|
||||
|
||||
// Apply widget options
|
||||
if (!empty($template['widget_options']) && is_array($template['widget_options'])) {
|
||||
foreach ($template['widget_options'] as $key => $value) {
|
||||
update_option($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sidebar assignments
|
||||
if (!empty($template['sidebars_widgets'])) {
|
||||
update_option('sidebars_widgets', $template['sidebars_widgets']);
|
||||
}
|
||||
|
||||
return $this->success(['message' => '模板已应用']);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /widgets/templates/{id} — Delete a saved template.
|
||||
*/
|
||||
public function delete_widget_template(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$templates = get_option('aksrt_widget_templates', []);
|
||||
|
||||
$found = false;
|
||||
$filtered = [];
|
||||
foreach ($templates as $t) {
|
||||
if ((int) ($t['id'] ?? 0) === $id) {
|
||||
$found = true;
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $t;
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
return $this->error('模板不存在', 404);
|
||||
}
|
||||
|
||||
update_option('aksrt_widget_templates', $filtered);
|
||||
return $this->success(['message' => '模板已删除']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all widget options from the database.
|
||||
*/
|
||||
private function collect_widget_options(): array {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'widget_aksrt_%' ORDER BY option_name",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$options = [];
|
||||
if ($rows) {
|
||||
foreach ($rows as $row) {
|
||||
$value = maybe_unserialize($row['option_value']);
|
||||
$options[$row['option_name']] = $value;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function persist_setting_backup(array $backup): void {
|
||||
$backups = get_option('aksrt_settings_backups', []);
|
||||
if (!is_array($backups)) {
|
||||
$backups = [];
|
||||
}
|
||||
|
||||
array_unshift($backups, $backup);
|
||||
update_option('aksrt_settings_backups', $this->trim_setting_backups($backups));
|
||||
}
|
||||
|
||||
private function create_backup_record(string $type, string $label = ''): array {
|
||||
return $this->build_settings_backup($type, $label);
|
||||
}
|
||||
|
||||
public function register_backup_schedules(array $schedules): array {
|
||||
if (!isset($schedules[self::MONTHLY_SCHEDULE])) {
|
||||
$schedules[self::MONTHLY_SCHEDULE] = [
|
||||
'interval' => MONTH_IN_SECONDS,
|
||||
'display' => __('每月', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
public function sync_backup_schedule(): void {
|
||||
$settings = $this->get_backup_schedule_settings();
|
||||
$next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
|
||||
|
||||
if (!$settings['enabled']) {
|
||||
if ($next) {
|
||||
wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$schedule = $settings['schedule'];
|
||||
if ($next && wp_get_schedule(self::SCHEDULED_BACKUP_HOOK) !== $schedule) {
|
||||
wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
|
||||
$next = false;
|
||||
}
|
||||
|
||||
if (!$next) {
|
||||
wp_schedule_event(time() + HOUR_IN_SECONDS, $schedule, self::SCHEDULED_BACKUP_HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
public function clear_backup_schedule(): void {
|
||||
$next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
|
||||
while ($next) {
|
||||
wp_unschedule_event($next, self::SCHEDULED_BACKUP_HOOK);
|
||||
$next = wp_next_scheduled(self::SCHEDULED_BACKUP_HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
public function run_scheduled_backup(): void {
|
||||
$settings = $this->get_backup_schedule_settings();
|
||||
if (!$settings['enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backup = $this->create_backup_record('scheduled');
|
||||
$this->persist_setting_backup($backup);
|
||||
}
|
||||
|
||||
public function handle_theme_upgrade_snapshot($response, array $hook_extra) {
|
||||
if (($hook_extra['type'] ?? '') !== 'theme') {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$themes = $hook_extra['themes'] ?? [];
|
||||
$stylesheet = wp_get_theme()->get_stylesheet();
|
||||
if (!in_array($stylesheet, (array) $themes, true)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$backup = $this->create_backup_record('update_theme');
|
||||
$this->persist_setting_backup($backup);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait ImportExportControllerPart2 {
|
||||
|
||||
|
||||
private function get_backup_schedule_settings(): array {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$settings = $provider->read_settings('tools', 'backup');
|
||||
$frequency = sanitize_key((string) ($settings['backup_frequency'] ?? 'weekly'));
|
||||
$allowed = ['daily', 'weekly', 'monthly'];
|
||||
if (!in_array($frequency, $allowed, true)) {
|
||||
$frequency = 'weekly';
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => !empty($settings['backup_auto']),
|
||||
'schedule' => $frequency === 'monthly' ? self::MONTHLY_SCHEDULE : $frequency,
|
||||
];
|
||||
}
|
||||
|
||||
private function build_settings_backup(string $type, string $label = ''): array {
|
||||
$schema_provider = SchemaProvider::get_instance();
|
||||
$domains = $schema_provider->get_domain_keys();
|
||||
|
||||
$payload = [
|
||||
'version' => AKSRT_VERSION ?? '3.4.0',
|
||||
'exported' => gmdate('Y-m-d H:i:s'),
|
||||
'domains' => [],
|
||||
'standalone'=> [],
|
||||
];
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$payload['domains'][$domain] = [
|
||||
'option_key' => $schema_provider->get_option_key($domain),
|
||||
'data' => $schema_provider->read_settings($domain),
|
||||
];
|
||||
}
|
||||
|
||||
foreach (\AKSRT\Settings\Settings::STANDALONE_OPTION_KEYS as $key) {
|
||||
$value = get_option($key);
|
||||
if ($value !== false) {
|
||||
$payload['standalone'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$payload = \AKSRT\Settings\Settings::redact_sensitive_settings($payload);
|
||||
|
||||
return [
|
||||
'id' => time(),
|
||||
'type' => $type,
|
||||
'label' => $label !== '' ? $label : $this->default_backup_label($type),
|
||||
'created_at' => gmdate('Y-m-d H:i:s'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
private function trim_setting_backups(array $backups): array {
|
||||
$retain = (int) (\AKSRT\Settings\Settings::get_value('backup_retain', 7));
|
||||
$retain = max(3, min(20, $retain));
|
||||
|
||||
if (count($backups) <= $retain) {
|
||||
return array_values($backups);
|
||||
}
|
||||
|
||||
return array_values(array_slice($backups, 0, $retain));
|
||||
}
|
||||
|
||||
private function default_backup_label(string $type): string {
|
||||
$map = [
|
||||
'manual' => __('手动备份', 'aksrt'),
|
||||
'scheduled' => __('定期备份', 'aksrt'),
|
||||
'reset_all' => __('重置全部前快照', 'aksrt'),
|
||||
'reset_domain' => __('重置分区前快照', 'aksrt'),
|
||||
'update_theme' => __('更新主题前快照', 'aksrt'),
|
||||
];
|
||||
|
||||
return $map[$type] ?? __('设置快照', 'aksrt');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class MessageController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// ── Notifications ──
|
||||
|
||||
// List notifications
|
||||
register_rest_route(self::API_NAMESPACE, '/messages', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'list_notifications'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
'args' => [
|
||||
'type' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Unread count
|
||||
register_rest_route(self::API_NAMESPACE, '/messages/unread', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_unread_count'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// Mark one notification read
|
||||
register_rest_route(self::API_NAMESPACE, '/messages/(?P<id>\d+)/read', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'mark_read'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// Mark all notifications read
|
||||
register_rest_route(self::API_NAMESPACE, '/messages/read-all', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'mark_all_read'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// ── Private Messages / Conversations ──
|
||||
|
||||
// List conversations
|
||||
register_rest_route(self::API_NAMESPACE, '/conversations', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'list_conversations'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
'args' => [
|
||||
'limit' => ['default' => 20, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Get thread messages
|
||||
register_rest_route(self::API_NAMESPACE, '/conversations/(?P<id>\d+)/messages', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_thread'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
'args' => [
|
||||
'page' => ['default' => 1, 'sanitize_callback' => 'absint'],
|
||||
'per_page' => ['default' => 30, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Send message in conversation
|
||||
register_rest_route(self::API_NAMESPACE, '/conversations/(?P<id>\d+)/messages', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'send_message'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
'args' => [
|
||||
'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create or get conversation with a user
|
||||
register_rest_route(self::API_NAMESPACE, '/conversations/with/(?P<user_id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'get_or_create_conversation'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// Mark conversation read
|
||||
register_rest_route(self::API_NAMESPACE, '/conversations/(?P<id>\d+)/read', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'mark_conversation_read'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Notification handlers ──
|
||||
|
||||
public function list_notifications(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$type = $request->get_param('type');
|
||||
$per_page = min(100, (int) $request->get_param('per_page'));
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\Message')) {
|
||||
return $this->success(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$msg = \AKSRT\Messages\Message::get_instance();
|
||||
$args = [
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
];
|
||||
if ($type) {
|
||||
$args['type'] = $type;
|
||||
}
|
||||
|
||||
$notifications = $msg->get_notifications($user_id, $args);
|
||||
$unread = $msg->get_unread_count($user_id);
|
||||
|
||||
return $this->success([
|
||||
'data' => $notifications,
|
||||
'unread' => $unread,
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_unread_count(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if (!class_exists('\AKSRT\Messages\Message')) {
|
||||
return $this->success(['data' => ['unread' => 0]]);
|
||||
}
|
||||
$count = \AKSRT\Messages\Message::get_instance()->get_unread_count($user_id);
|
||||
return $this->success(['data' => ['unread' => $count]]);
|
||||
}
|
||||
|
||||
public function mark_read(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$id = (string) $request->get_param('id');
|
||||
if (!class_exists('\AKSRT\Messages\Message')) {
|
||||
return $this->error('not_available', '消息系统未启用。', 501);
|
||||
}
|
||||
$result = \AKSRT\Messages\Message::get_instance()->mark_read($user_id, $id);
|
||||
return $this->success(['data' => ['success' => $result]]);
|
||||
}
|
||||
|
||||
public function mark_all_read(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if (!class_exists('\AKSRT\Messages\Message')) {
|
||||
return $this->error('not_available', '消息系统未启用。', 501);
|
||||
}
|
||||
$result = \AKSRT\Messages\Message::get_instance()->mark_all_read($user_id);
|
||||
return $this->success(['data' => ['success' => $result]]);
|
||||
}
|
||||
|
||||
// ── Conversation handlers ──
|
||||
|
||||
public function list_conversations(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$limit = min(50, (int) $request->get_param('limit'));
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
return $this->success(['data' => []]);
|
||||
}
|
||||
|
||||
$conversations = \AKSRT\Messages\PrivateMessage::get_instance()->get_conversations($user_id, $limit);
|
||||
return $this->success(['data' => $conversations]);
|
||||
}
|
||||
|
||||
public function get_thread(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$conv_id = (int) $request->get_param('id');
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(50, (int) $request->get_param('per_page'));
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$result = \AKSRT\Messages\PrivateMessage::get_instance()->get_thread($conv_id, $user_id, $page, $per_page);
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error('thread_error', $result->get_error_message(), 403);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function send_message(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$conv_id = (int) $request->get_param('id');
|
||||
$content = trim((string) $request->get_param('content'));
|
||||
|
||||
if (!$content) {
|
||||
return $this->error('content_required', '消息内容不能为空。', 400);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$result = \AKSRT\Messages\PrivateMessage::get_instance()->send($conv_id, $user_id, $content);
|
||||
if (!$result['success']) {
|
||||
return $this->error('send_failed', $result['message'] ?? '发送失败。', 500);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function get_or_create_conversation(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$other_id = (int) $request->get_param('user_id');
|
||||
|
||||
if ($user_id === $other_id) {
|
||||
return $this->error('self_message', '不能与自己创建对话。', 400);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$conv_id = \AKSRT\Messages\PrivateMessage::get_instance()->get_or_create_conversation($user_id, $other_id);
|
||||
if (is_wp_error($conv_id)) {
|
||||
return $this->error('create_failed', $conv_id->get_error_message(), 500);
|
||||
}
|
||||
|
||||
return $this->success(['data' => ['conversation_id' => $conv_id]]);
|
||||
}
|
||||
|
||||
public function mark_conversation_read(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$conv_id = (int) $request->get_param('id');
|
||||
|
||||
if (!class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
\AKSRT\Messages\PrivateMessage::get_instance()->mark_conversation_read($conv_id, $user_id);
|
||||
return $this->success(['data' => ['success' => true]]);
|
||||
}
|
||||
|
||||
public function check_logged_in_permission(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayChargeCards {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_charge_cards';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_disable_charge_card', [$this, 'handle_disable_charge_card']);
|
||||
add_action('admin_post_aksrt_pay_batch_charge_cards', [$this, 'handle_batch_charge_cards']);
|
||||
}
|
||||
|
||||
public function render_charge_cards_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('卡密管理', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\ChargeCard::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
$type_labels = [
|
||||
'balance' => __('余额', 'aksrt'),
|
||||
'vip' => __('VIP', 'aksrt'),
|
||||
'credits' => __('积分', 'aksrt'),
|
||||
'points' => __('积分', 'aksrt'),
|
||||
];
|
||||
$status_labels = [
|
||||
'active' => __('可用', 'aksrt'),
|
||||
'used' => __('已用', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
'expired' => __('已过期', 'aksrt'),
|
||||
];
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: active 3: used */
|
||||
__('共 %1$d 张 · 可用 %2$d · 已用 %3$d', 'aksrt'),
|
||||
(int) ($stats['total'] ?? 0),
|
||||
(int) ($stats['active'] ?? 0),
|
||||
(int) ($stats['used'] ?? 0)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<h2>' . esc_html__('批量生成卡密', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_charge_cards" />';
|
||||
wp_nonce_field('aksrt_pay_batch_charge_cards');
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
|
||||
echo '<option value="balance">' . esc_html__('余额', 'aksrt') . '</option>';
|
||||
echo '<option value="vip">' . esc_html__('VIP', 'aksrt') . '</option>';
|
||||
echo '<option value="credits">' . esc_html__('积分', 'aksrt') . '</option>';
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="500" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="8" max="32" value="16" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('面额/积分', 'aksrt') . '<br><input type="number" step="0.01" name="amount" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('VIP等级', 'aksrt') . '<br><input type="number" name="vip_level" min="1" max="2" value="1" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('VIP天数', 'aksrt') . '<br><input type="number" name="vip_days" min="1" value="30" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" value="" /></label></p>';
|
||||
submit_button(__('生成', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
if (!empty($_GET['aksrt_generated'])) {
|
||||
$generated = (int) $_GET['aksrt_generated'];
|
||||
echo '<div class="notice notice-success"><p>' . esc_html(sprintf(
|
||||
/* translators: %d count */
|
||||
__('成功生成 %d 张卡密。', 'aksrt'),
|
||||
$generated
|
||||
)) . '</p></div>';
|
||||
}
|
||||
|
||||
$generated_codes = get_transient('aksrt_pay_card_codes_' . get_current_user_id());
|
||||
if (is_array($generated_codes) && $generated_codes) {
|
||||
delete_transient('aksrt_pay_card_codes_' . get_current_user_id());
|
||||
echo '<div class="notice notice-info"><p><strong>' . esc_html__('本次生成卡密(请及时复制保存):', 'aksrt') . '</strong></p>';
|
||||
echo '<textarea class="large-text code" rows="' . esc_attr((string) min(12, max(3, count($generated_codes)))) . '" readonly onclick="this.select();">' . esc_textarea(implode("\n", $generated_codes)) . '</textarea>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_charge_cards" />';
|
||||
echo '<select name="status">';
|
||||
foreach ([
|
||||
'' => __('全部状态', 'aksrt'),
|
||||
'active' => __('可用', 'aksrt'),
|
||||
'used' => __('已用', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
'expired' => __('已过期', 'aksrt'),
|
||||
] as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($status, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<select name="type">';
|
||||
foreach ([
|
||||
'' => __('全部类型', 'aksrt'),
|
||||
'balance' => __('余额', 'aksrt'),
|
||||
'vip' => __('VIP', 'aksrt'),
|
||||
'credits' => __('积分', 'aksrt'),
|
||||
] as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($type, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('密码 / 卡号 / 备注', 'aksrt') . '" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
$result = $svc->get_list([
|
||||
'status' => $status,
|
||||
'type' => $type,
|
||||
'search' => $search,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
]);
|
||||
$items = $result['items'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$pages = max(1, (int) ($result['max_pages'] ?? 1));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>ID</th>';
|
||||
echo '<th>' . esc_html__('类型', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('密码', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('面额', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('状态', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('过期', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('使用者', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('创建时间', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="9">' . esc_html__('暂无卡密', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
$row_type = (string) ($row['type'] ?? '');
|
||||
$user_id = (int) ($row['used_by'] ?? $row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . esc_html($type_labels[$row_type] ?? $row_type) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['password'] ?? $row['code'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html((string) ($row['amount'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($status_labels[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '—')) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name) : '—') . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($st === 'active') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_disable_charge_card" />';
|
||||
echo '<input type="hidden" name="card_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_disable_charge_card_' . $id);
|
||||
submit_button(__('禁用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
public function handle_disable_charge_card(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$id = isset($_POST['card_id']) ? (int) $_POST['card_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_disable_charge_card_' . $id);
|
||||
|
||||
$ok = false;
|
||||
if ($id > 0 && class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
$ok = \AKSRT\Payment\ChargeCard::get_instance()->disable($id);
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', [
|
||||
'aksrt_pay_msg' => $ok ? 'card_disabled' : 'card_failed',
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_batch_charge_cards(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_pay_batch_charge_cards');
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'balance';
|
||||
$count = isset($_POST['count']) ? (int) $_POST['count'] : 10;
|
||||
$length = isset($_POST['length']) ? (int) $_POST['length'] : 16;
|
||||
$prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : '';
|
||||
$amount = isset($_POST['amount']) ? (float) wp_unslash($_POST['amount']) : 0;
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
$level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1;
|
||||
$days = isset($_POST['vip_days']) ? max(1, (int) $_POST['vip_days']) : 30;
|
||||
$expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : '';
|
||||
if ($expires !== '') {
|
||||
$expires = str_replace('T', ' ', $expires);
|
||||
if (strlen($expires) === 16) {
|
||||
$expires .= ':00';
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'points') {
|
||||
$type = 'credits';
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if ($type === 'balance') {
|
||||
$meta['amount'] = max(0, $amount);
|
||||
} elseif ($type === 'credits') {
|
||||
$meta['points'] = max(0, (int) $amount);
|
||||
} elseif ($type === 'vip') {
|
||||
$meta['level'] = $level;
|
||||
$meta['days'] = $days;
|
||||
}
|
||||
|
||||
$codes = \AKSRT\Payment\ChargeCard::get_instance()->batch_create($type, $meta, $count, $length, $prefix, $expires, $note);
|
||||
$ok = !empty($codes);
|
||||
if ($ok) {
|
||||
set_transient('aksrt_pay_card_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', [
|
||||
'aksrt_pay_msg' => $ok ? 'card_batch_ok' : 'card_batch_failed',
|
||||
'aksrt_generated' => count($codes),
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'card_disabled' => __('充值卡已禁用。', 'aksrt'),
|
||||
'card_failed' => __('充值卡禁用失败。', 'aksrt'),
|
||||
'card_batch_ok' => __('卡密批量生成完成。', 'aksrt'),
|
||||
'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayCoupons {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_coupons';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_create_coupon', [$this, 'handle_create_coupon']);
|
||||
add_action('admin_post_aksrt_pay_batch_coupons', [$this, 'handle_batch_coupons']);
|
||||
add_action('admin_post_aksrt_pay_update_coupon', [$this, 'handle_update_coupon']);
|
||||
add_action('admin_post_aksrt_pay_disable_coupon', [$this, 'handle_disable_coupon']);
|
||||
add_action('admin_post_aksrt_pay_enable_coupon', [$this, 'handle_enable_coupon']);
|
||||
}
|
||||
|
||||
public function render_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('优惠码', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Coupon')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$svc = \AKSRT\Payment\Coupon::get_instance();
|
||||
$edit_id = isset($_GET['edit_id']) ? (int) $_GET['edit_id'] : 0;
|
||||
$edit_row = $edit_id > 0 ? $svc->get($edit_id) : null;
|
||||
|
||||
if ($edit_id > 0 && $edit_row) {
|
||||
$this->render_edit_form($edit_row);
|
||||
} elseif ($edit_id > 0) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('优惠码不存在。', 'aksrt') . '</p></div>';
|
||||
}
|
||||
|
||||
$this->render_create_form();
|
||||
$this->render_batch_form();
|
||||
$this->render_generated_dump();
|
||||
$this->render_list($svc);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $row
|
||||
*/
|
||||
private function render_edit_form(array $row): void {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$scope = is_array($row['product_scope'] ?? null) ? $row['product_scope'] : [];
|
||||
$scope_type = sanitize_key((string) ($scope['type'] ?? ''));
|
||||
$scope_ids = array_map('intval', (array) ($scope['ids'] ?? []));
|
||||
$expires = (string) ($row['expires_at'] ?? '');
|
||||
$expires_local = $expires !== '' ? str_replace(' ', 'T', substr($expires, 0, 16)) : '';
|
||||
|
||||
echo '<h2>' . esc_html(sprintf(
|
||||
/* translators: %d coupon id */
|
||||
__('编辑优惠码 #%d', 'aksrt'),
|
||||
$id
|
||||
)) . '</h2>';
|
||||
echo '<p><a href="' . esc_url($this->page_url()) . '">← ' . esc_html__('返回列表', 'aksrt') . '</a></p>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_coupon_' . $id);
|
||||
echo '<p><label>' . esc_html__('代码', 'aksrt') . '<br><input type="text" value="' . esc_attr((string) ($row['code'] ?? '')) . '" class="regular-text" readonly /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
|
||||
foreach (['fixed' => __('固定金额', 'aksrt'), 'percent' => __('折扣百分比', 'aksrt')] as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected((string) ($row['type'] ?? ''), $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="' . esc_attr((string) ($row['value'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="' . esc_attr((string) ($row['min_amount'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠(百分比用)', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="' . esc_attr((string) ($row['max_discount'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="' . esc_attr((string) ($row['usage_limit'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次(0=不限)', 'aksrt') . '<br><input type="number" name="per_user_limit" value="' . esc_attr((string) ($row['per_user_limit'] ?? 0)) . '" /></label></p>';
|
||||
$this->render_scope_fields($scope_type, implode(',', $scope_ids));
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" value="' . esc_attr($expires_local) . '" /></label></p>';
|
||||
submit_button(__('保存修改', 'aksrt'), 'primary', '', false);
|
||||
echo '</form><hr>';
|
||||
}
|
||||
|
||||
private function render_create_form(): void {
|
||||
echo '<h2>' . esc_html__('新建优惠码', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_create_coupon" />';
|
||||
wp_nonce_field('aksrt_pay_create_coupon');
|
||||
echo '<p><label>' . esc_html__('代码', 'aksrt') . '<br><input type="text" name="code" required /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type"><option value="fixed">' . esc_html__('固定金额', 'aksrt') . '</option><option value="percent">' . esc_html__('折扣百分比', 'aksrt') . '</option></select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠(百分比用)', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次(0=不限)', 'aksrt') . '<br><input type="number" name="per_user_limit" value="0" /></label></p>';
|
||||
$this->render_scope_fields('', '');
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
submit_button(__('创建', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_batch_form(): void {
|
||||
echo '<h2>' . esc_html__('批量生成优惠码', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_coupons" />';
|
||||
wp_nonce_field('aksrt_pay_batch_coupons');
|
||||
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="100" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="6" max="24" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type"><option value="fixed">' . esc_html__('固定金额', 'aksrt') . '</option><option value="percent">' . esc_html__('折扣百分比', 'aksrt') . '</option></select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="1" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次', 'aksrt') . '<br><input type="number" name="per_user_limit" value="1" /></label></p>';
|
||||
$this->render_scope_fields('', '');
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
submit_button(__('批量生成', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_scope_fields(string $selected_type, string $ids_text): void {
|
||||
echo '<p><label>' . esc_html__('适用范围', 'aksrt') . '<br><select name="scope_type">';
|
||||
foreach ([
|
||||
'' => __('全部商品', 'aksrt'),
|
||||
'include' => __('仅指定商品 ID', 'aksrt'),
|
||||
'exclude' => __('排除商品 ID', 'aksrt'),
|
||||
'category' => __('指定分类 ID', 'aksrt'),
|
||||
] as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($selected_type, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('范围 ID', 'aksrt') . '<br><input type="text" name="scope_ids" class="regular-text" value="' . esc_attr($ids_text) . '" placeholder="' . esc_attr__('逗号分隔,如 12,34', 'aksrt') . '" /></label></p>';
|
||||
}
|
||||
|
||||
private function render_generated_dump(): void {
|
||||
if (!empty($_GET['aksrt_coupon_generated'])) {
|
||||
$generated = (int) $_GET['aksrt_coupon_generated'];
|
||||
echo '<div class="notice notice-success"><p>' . esc_html(sprintf(
|
||||
/* translators: %d count */
|
||||
__('成功生成 %d 个优惠码。', 'aksrt'),
|
||||
$generated
|
||||
)) . '</p></div>';
|
||||
}
|
||||
$generated_codes = get_transient('aksrt_pay_coupon_codes_' . get_current_user_id());
|
||||
if (is_array($generated_codes) && $generated_codes) {
|
||||
delete_transient('aksrt_pay_coupon_codes_' . get_current_user_id());
|
||||
echo '<div class="notice notice-info"><p><strong>' . esc_html__('本次生成优惠码(请及时复制保存):', 'aksrt') . '</strong></p>';
|
||||
echo '<textarea class="large-text code" rows="' . esc_attr((string) min(12, max(3, count($generated_codes)))) . '" readonly onclick="this.select();">' . esc_textarea(implode("\n", $generated_codes)) . '</textarea>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
private function render_list(\AKSRT\Payment\Coupon $svc): void {
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$type_labels = [
|
||||
'fixed' => __('固定金额', 'aksrt'),
|
||||
'percent' => __('折扣百分比', 'aksrt'),
|
||||
];
|
||||
$status_labels = [
|
||||
'active' => __('可用', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
];
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'active' => __('可用', 'aksrt'), 'disabled' => __('已禁用', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索代码', 'aksrt') . '" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
$result = $svc->get_list([
|
||||
'status' => $status,
|
||||
'search' => $search,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
]);
|
||||
$items = $result['items'] ?? [];
|
||||
$pages = max(1, (int) ($result['total_pages'] ?? 1));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="10">' . esc_html__('暂无优惠码', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
$row_type = (string) ($row['type'] ?? '');
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['code'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html($type_labels[$row_type] ?? $row_type) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['value'] ?? '')) . ($row_type === 'percent' ? '%' : '') . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['min_amount'] ?? 0)) . ' / ' . esc_html((string) ($row['max_discount'] ?? 0)) . '</td>';
|
||||
echo '<td>' . esc_html($this->format_scope_label($row['product_scope'] ?? [])) . '</td>';
|
||||
echo '<td>' . esc_html((string) ((int) ($row['used_count'] ?? 0)) . '/' . (string) ((int) ($row['usage_limit'] ?? 0)) . '/' . (string) ((int) ($row['per_user_limit'] ?? 0))) . '</td>';
|
||||
echo '<td>' . esc_html($status_labels[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<a class="button button-small" href="' . esc_url($this->page_url(['edit_id' => $id])) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||
if ($st === 'active') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_disable_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_disable_coupon_' . $id);
|
||||
submit_button(__('禁用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} elseif ($st === 'disabled') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_enable_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_enable_coupon_' . $id);
|
||||
submit_button(__('启用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
}
|
||||
|
||||
public function handle_create_coupon(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_pay_create_coupon');
|
||||
|
||||
$ok = false;
|
||||
if (class_exists('\AKSRT\Payment\Coupon')) {
|
||||
$id = \AKSRT\Payment\Coupon::get_instance()->create([
|
||||
'code' => sanitize_text_field((string) wp_unslash($_POST['code'] ?? '')),
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
'expires_at' => $this->normalize_expires_from_request(),
|
||||
]);
|
||||
$ok = $id > 0;
|
||||
}
|
||||
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_created' : 'coupon_create_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_batch_coupons(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_pay_batch_coupons');
|
||||
|
||||
$ok = false;
|
||||
$created = 0;
|
||||
$codes = [];
|
||||
if (class_exists('\AKSRT\Payment\Coupon')) {
|
||||
$result = \AKSRT\Payment\Coupon::get_instance()->batch_create([
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
'expires_at' => $this->normalize_expires_from_request(),
|
||||
], (int) ($_POST['count'] ?? 10), (int) ($_POST['length'] ?? 10), sanitize_text_field((string) wp_unslash($_POST['prefix'] ?? '')));
|
||||
$created = (int) ($result['created'] ?? 0);
|
||||
$codes = is_array($result['codes'] ?? null) ? $result['codes'] : [];
|
||||
$ok = $created > 0;
|
||||
if ($ok) {
|
||||
set_transient('aksrt_pay_coupon_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url([
|
||||
'aksrt_pay_msg' => $ok ? 'coupon_batch_ok' : 'coupon_batch_failed',
|
||||
'aksrt_coupon_generated' => $created,
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_update_coupon(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_update_coupon_' . $id);
|
||||
|
||||
$ok = false;
|
||||
if ($id > 0 && class_exists('\AKSRT\Payment\Coupon')) {
|
||||
$ok = \AKSRT\Payment\Coupon::get_instance()->update($id, [
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
'expires_at' => $this->normalize_expires_from_request(),
|
||||
]);
|
||||
}
|
||||
wp_safe_redirect($this->page_url([
|
||||
'aksrt_pay_msg' => $ok ? 'coupon_updated' : 'coupon_update_failed',
|
||||
'edit_id' => $id,
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_disable_coupon(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_disable_coupon_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Coupon') && \AKSRT\Payment\Coupon::get_instance()->disable($id);
|
||||
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_disabled' : 'coupon_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_enable_coupon(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['coupon_id']) ? (int) $_POST['coupon_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_enable_coupon_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Coupon') && \AKSRT\Payment\Coupon::get_instance()->enable($id);
|
||||
wp_safe_redirect($this->page_url(['aksrt_pay_msg' => $ok ? 'coupon_enabled' : 'coupon_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function parse_product_scope_from_request(): array {
|
||||
$type = isset($_POST['scope_type']) ? sanitize_key((string) wp_unslash($_POST['scope_type'])) : '';
|
||||
if ($type === '' || !in_array($type, ['include', 'exclude', 'category'], true)) {
|
||||
return [];
|
||||
}
|
||||
$raw = isset($_POST['scope_ids']) ? (string) wp_unslash($_POST['scope_ids']) : '';
|
||||
$parts = preg_split('/[\s,;]+/', $raw) ?: [];
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $parts))));
|
||||
if (!$ids) {
|
||||
return [];
|
||||
}
|
||||
$scope = [
|
||||
'type' => $type,
|
||||
'ids' => $ids,
|
||||
];
|
||||
if ($type === 'category') {
|
||||
$scope['taxonomies'] = ['category'];
|
||||
}
|
||||
return $scope;
|
||||
}
|
||||
|
||||
private function normalize_expires_from_request(): string {
|
||||
$expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : '';
|
||||
if ($expires === '') {
|
||||
return '';
|
||||
}
|
||||
$expires = str_replace('T', ' ', $expires);
|
||||
if (strlen($expires) === 16) {
|
||||
$expires .= ':00';
|
||||
}
|
||||
return $expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $scope
|
||||
*/
|
||||
private function format_scope_label($scope): string {
|
||||
if (is_string($scope) && $scope !== '') {
|
||||
$decoded = json_decode($scope, true);
|
||||
$scope = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($scope) || empty($scope)) {
|
||||
return __('全部', 'aksrt');
|
||||
}
|
||||
$type = sanitize_key((string) ($scope['type'] ?? ''));
|
||||
$ids = array_map('intval', (array) ($scope['ids'] ?? []));
|
||||
$id_text = $ids ? implode(',', $ids) : '—';
|
||||
$map = [
|
||||
'include' => __('含商品', 'aksrt'),
|
||||
'exclude' => __('排除商品', 'aksrt'),
|
||||
'category' => __('分类', 'aksrt'),
|
||||
];
|
||||
$label = $map[$type] ?? $type;
|
||||
return $label . ':' . $id_text;
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'coupon_created' => __('优惠码已创建。', 'aksrt'),
|
||||
'coupon_create_failed' => __('优惠码创建失败(可能重复)。', 'aksrt'),
|
||||
'coupon_batch_ok' => __('优惠码批量生成完成。', 'aksrt'),
|
||||
'coupon_batch_failed' => __('优惠码批量生成失败。', 'aksrt'),
|
||||
'coupon_updated' => __('优惠码已更新。', 'aksrt'),
|
||||
'coupon_update_failed' => __('优惠码更新失败。', 'aksrt'),
|
||||
'coupon_disabled' => __('优惠码已禁用。', 'aksrt'),
|
||||
'coupon_enabled' => __('优惠码已启用。', 'aksrt'),
|
||||
'coupon_failed' => __('优惠码操作失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $args
|
||||
*/
|
||||
private function page_url(array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayDownloadLogs {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_download_logs';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Read-only.
|
||||
}
|
||||
|
||||
public function render_page(): void {
|
||||
$this->ensure_cap();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('下载记录', 'aksrt') . '</h1>';
|
||||
|
||||
if (!class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('数据库服务不可用。', 'aksrt') . '</p></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('dl_token');
|
||||
if ($table === '') {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('下载令牌表未初始化。', 'aksrt') . '</p></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0;
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
|
||||
$only_used = !isset($_GET['used']) || (string) $_GET['used'] !== '0';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 30;
|
||||
|
||||
$result = $this->query_logs($table, [
|
||||
'post_id' => $post_id,
|
||||
'user_id' => $user_id,
|
||||
'only_used' => $only_used,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
]);
|
||||
$items = $result['items'];
|
||||
$total = $result['total'];
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
|
||||
echo '<p class="description">' . esc_html__('基于下载令牌表(aksrt_dl_token)。已使用记录会保留一段时间后清理;未使用且过期的令牌会被定期删除。', 'aksrt') . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
echo '<label>' . esc_html__('文章 ID', 'aksrt') . ' <input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" class="small-text" /></label> ';
|
||||
echo '<label>' . esc_html__('用户 ID', 'aksrt') . ' <input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" class="small-text" /></label> ';
|
||||
echo '<label><input type="checkbox" name="used" value="1" ' . checked($only_used, true, false) . ' /> ' . esc_html__('仅已使用', 'aksrt') . '</label> ';
|
||||
// When unchecked, pass used=0 explicitly.
|
||||
if (!$only_used) {
|
||||
// no-op; checkbox not checked
|
||||
}
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo ' <a class="button" href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG)) . '">' . esc_html__('重置', 'aksrt') . '</a>';
|
||||
echo '</form>';
|
||||
|
||||
// Fix: unchecked checkbox means used filter off — re-read without default if form submitted with no used.
|
||||
// Already handled via isset($_GET['used']).
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('文章', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('源序号', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('积分消耗', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('使用', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('过期', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('IP', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="8">' . esc_html__('暂无下载记录。', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$uid = (int) ($row['user_id'] ?? 0);
|
||||
$pid = (int) ($row['post_id'] ?? 0);
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
$title = $pid > 0 ? get_the_title($pid) : '';
|
||||
if ($title === '') {
|
||||
$title = $pid > 0 ? sprintf(__('(已删除 #%d)', 'aksrt'), $pid) : '—';
|
||||
}
|
||||
$edit = $pid > 0 ? get_edit_post_link($pid, 'raw') : '';
|
||||
$used = (int) ($row['used_count'] ?? 0);
|
||||
$max = (int) ($row['max_uses'] ?? 1);
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($user) {
|
||||
echo esc_html($user->display_name . ' (#' . $uid . ')');
|
||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&user_id=' . $uid)) . '">' . esc_html__('仅此用户', 'aksrt') . '</a>';
|
||||
} else {
|
||||
echo esc_html((string) $uid);
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<td>';
|
||||
if ($edit) {
|
||||
echo '<a href="' . esc_url($edit) . '">' . esc_html($title) . '</a>';
|
||||
} else {
|
||||
echo esc_html($title);
|
||||
}
|
||||
if ($pid > 0) {
|
||||
echo '<br><a href="' . esc_url(admin_url('admin.php?page=' . self::PAGE_SLUG . '&post_id=' . $pid)) . '">' . esc_html__('仅此文章', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<td>' . esc_html((string) (int) ($row['source_index'] ?? 0)) . '</td>';
|
||||
echo '<td>' . esc_html((string) (int) ($row['cost'] ?? 0)) . '</td>';
|
||||
echo '<td>' . esc_html($used . ' / ' . $max) . ($used > 0 ? ' <span class="dashicons dashicons-yes" style="color:green;"></span>' : '') . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '')) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['ip_bind'] ?? '')) . '</code></td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{post_id?:int,user_id?:int,only_used?:bool,limit?:int,offset?:int} $args
|
||||
* @return array{items: array<int,array<string,mixed>>, total:int}
|
||||
*/
|
||||
private function query_logs(string $table, array $args): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
$post_id = (int) ($args['post_id'] ?? 0);
|
||||
if ($post_id > 0) {
|
||||
$where[] = 'post_id = %d';
|
||||
$params[] = $post_id;
|
||||
}
|
||||
$user_id = (int) ($args['user_id'] ?? 0);
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
if (!empty($args['only_used'])) {
|
||||
$where[] = 'used_count > 0';
|
||||
}
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 30)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
$total = $params
|
||||
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
|
||||
: (int) $wpdb->get_var($count_sql);
|
||||
|
||||
$sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$list_params = $params;
|
||||
$list_params[] = $limit;
|
||||
$list_params[] = $offset;
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $list_params), ARRAY_A);
|
||||
|
||||
return [
|
||||
'items' => is_array($rows) ? $rows : [],
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayIncome {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_income';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']);
|
||||
}
|
||||
|
||||
public function render_income_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('创作分成', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Revenue')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('分成模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\Revenue::get_instance();
|
||||
$stats = $user_id > 0 ? $svc->get_stats($user_id) : $svc->get_stats();
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: settled */
|
||||
__('合计 %1$s · 待结算 %2$s · 已结算 %3$s', 'aksrt'),
|
||||
number_format((float) ($stats['total'] ?? 0), 2),
|
||||
number_format((float) ($stats['pending'] ?? 0), 2),
|
||||
number_format((float) ($stats['settled'] ?? 0), 2)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_income" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
if ($user_id > 0) {
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_income')) . '">' . esc_html__('清除用户过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
|
||||
$list_args = [
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
];
|
||||
if ($user_id > 0) {
|
||||
$list_args['user_id'] = $user_id;
|
||||
}
|
||||
$result = $svc->get_list($list_args);
|
||||
$items = $result['items'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$pages = max(1, (int) ($result['max_pages'] ?? 1));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($st === 'pending') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_settle_income" />';
|
||||
echo '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_settle_income_' . $id);
|
||||
submit_button(__('结算', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
public function handle_settle_income(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_settle_income_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Revenue') && \AKSRT\Payment\Revenue::get_instance()->settle($id);
|
||||
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'income_settled' => __('分成已结算。', 'aksrt'),
|
||||
'income_failed' => __('分成结算失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayMenus {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const MENU_SLUG = 'aksrt_pay';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action('admin_menu', [$this, 'register_menus']);
|
||||
add_action('admin_notices', [$this, 'pending_notices']);
|
||||
// Domain admin_post handlers are registered by dedicated modules.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{slug:string,title:string,callback:callable}>
|
||||
*/
|
||||
private function pages(): array {
|
||||
return [
|
||||
'overview' => [
|
||||
'slug' => self::MENU_SLUG,
|
||||
'title' => __('概览', 'aksrt'),
|
||||
'callback' => [$this, 'render_overview_page'],
|
||||
],
|
||||
'orders' => [
|
||||
'slug' => 'aksrt_orders',
|
||||
'title' => __('订单', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\Admin\Native\PayOrders')) {
|
||||
PayOrders::get_instance()->render_orders_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('订单模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'products' => [
|
||||
'slug' => 'aksrt_pay_products',
|
||||
'title' => __('付费文章', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayProducts')) {
|
||||
PayProducts::get_instance()->render_products_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('付费文章模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'download_logs' => [
|
||||
'slug' => 'aksrt_download_logs',
|
||||
'title' => __('下载记录', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayDownloadLogs')) {
|
||||
PayDownloadLogs::get_instance()->render_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('下载记录模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'coupons' => [
|
||||
'slug' => 'aksrt_coupons',
|
||||
'title' => __('优惠码', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayCoupons')) {
|
||||
PayCoupons::get_instance()->render_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('优惠码模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'charge_cards' => [
|
||||
'slug' => 'aksrt_charge_cards',
|
||||
'title' => __('卡密管理', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayChargeCards')) {
|
||||
PayChargeCards::get_instance()->render_charge_cards_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('卡密模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'income' => [
|
||||
'slug' => 'aksrt_income',
|
||||
'title' => __('创作分成', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayIncome')) {
|
||||
PayIncome::get_instance()->render_income_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('分成模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'rebate' => [
|
||||
'slug' => 'aksrt_rebate',
|
||||
'title' => __('推广返佣', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayRebate')) {
|
||||
PayRebate::get_instance()->render_rebate_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('返佣模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'withdraw' => [
|
||||
'slug' => 'aksrt_withdraw',
|
||||
'title' => __('提现审核', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayWithdrawals')) {
|
||||
PayWithdrawals::get_instance()->render_withdrawals_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('提现模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function register_menus(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_menu_page(
|
||||
__('支付中心', 'aksrt'),
|
||||
__('支付中心', 'aksrt'),
|
||||
self::CAPABILITY,
|
||||
self::MENU_SLUG,
|
||||
[$this, 'render_overview_page'],
|
||||
'dashicons-cart',
|
||||
36
|
||||
);
|
||||
|
||||
foreach ($this->pages() as $page) {
|
||||
add_submenu_page(
|
||||
self::MENU_SLUG,
|
||||
$page['title'],
|
||||
$page['title'],
|
||||
self::CAPABILITY,
|
||||
$page['slug'],
|
||||
$page['callback']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function pending_notices(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
|
||||
|
||||
if ($page !== 'aksrt_withdraw' && class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$pending = (int) \AKSRT\Payment\Withdrawal::get_instance()->get_count(['status' => \AKSRT\Payment\Withdrawal::STATUS_PENDING]);
|
||||
if ($pending > 0) {
|
||||
$url = admin_url('admin.php?page=aksrt_withdraw&status=pending');
|
||||
echo '<div class="notice notice-info is-dismissible">';
|
||||
echo '<h3>' . esc_html__('提现申请待处理', 'aksrt') . '</h3>';
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: %d: pending withdrawal count */
|
||||
__('您有 %d 个提现申请待处理', 'aksrt'),
|
||||
$pending
|
||||
)) . '</p>';
|
||||
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
public function render_overview_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('支付中心', 'aksrt'));
|
||||
|
||||
$order_stats = class_exists('\AKSRT\Payment\OrderRepository')
|
||||
? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()
|
||||
: [];
|
||||
$withdraw_stats = class_exists('\AKSRT\Payment\Withdrawal')
|
||||
? \AKSRT\Payment\Withdrawal::get_instance()->get_stats()
|
||||
: [];
|
||||
$card_stats = class_exists('\AKSRT\Payment\ChargeCard')
|
||||
? \AKSRT\Payment\ChargeCard::get_instance()->get_stats()
|
||||
: [];
|
||||
|
||||
echo '<p class="description">' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
|
||||
|
||||
echo '<div class="aksrt-pay-stats" style="display:flex;flex-wrap:wrap;gap:16px;margin:20px 0;">';
|
||||
$this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders'));
|
||||
$this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']));
|
||||
$this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid']));
|
||||
$this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders'));
|
||||
$this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']));
|
||||
$this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']));
|
||||
echo '</div>';
|
||||
|
||||
echo '<h2>' . esc_html__('快捷入口', 'aksrt') . '</h2><ul>';
|
||||
foreach ($this->pages() as $page) {
|
||||
if ($page['slug'] === self::MENU_SLUG) {
|
||||
continue;
|
||||
}
|
||||
echo '<li><a href="' . esc_url($this->page_url($page['slug'])) . '">' . esc_html($page['title']) . '</a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
|
||||
$spa = admin_url('admin.php?page=aksrt-settings#/payment/gateway');
|
||||
echo '<p><a class="button" href="' . esc_url($spa) . '">' . esc_html__('打开支付配置(SPA)', 'aksrt') . '</a></p>';
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
private function stat_card(string $label, string $value, string $url): void {
|
||||
echo '<a href="' . esc_url($url) . '" style="display:block;min-width:140px;padding:16px;background:#fff;border:1px solid #c3c4c7;border-radius:4px;text-decoration:none;color:inherit;">';
|
||||
echo '<div style="font-size:12px;color:#646970;">' . esc_html($label) . '</div>';
|
||||
echo '<div style="font-size:22px;font-weight:600;margin-top:6px;">' . esc_html($value) . '</div>';
|
||||
echo '</a>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayOrders {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_orders';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_update_order_status', [$this, 'handle_update_order_status']);
|
||||
add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']);
|
||||
add_action('admin_post_aksrt_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']);
|
||||
add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']);
|
||||
}
|
||||
|
||||
public function render_orders_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('订单', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : '';
|
||||
if ($detail_order !== '') {
|
||||
$this->render_order_detail_page($detail_order);
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$product_type = isset($_GET['product_type']) ? sanitize_key((string) wp_unslash($_GET['product_type'])) : '';
|
||||
$product_id = isset($_GET['product_id']) ? max(0, (int) $_GET['product_id']) : 0;
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
|
||||
$pay_method = isset($_GET['payment_method']) ? sanitize_key((string) wp_unslash($_GET['payment_method'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$args = [
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
];
|
||||
if ($status !== '' && $status !== 'all') {
|
||||
$args['status'] = $status;
|
||||
}
|
||||
if ($product_type !== '') {
|
||||
$args['product_type'] = $product_type;
|
||||
}
|
||||
if ($product_id > 0) {
|
||||
$args['product_id'] = $product_id;
|
||||
}
|
||||
if ($user_id > 0) {
|
||||
$args['user_id'] = $user_id;
|
||||
}
|
||||
if ($pay_method !== '') {
|
||||
$args['payment_method'] = $pay_method;
|
||||
}
|
||||
|
||||
$items = $repo->get_orders_admin($args, $search);
|
||||
$total = $repo->get_orders_count($args, $search);
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
$labels = class_exists('\AKSRT\Core\DomainModel')
|
||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||
: [];
|
||||
|
||||
// Type tabs (zibll order subsubsub parity).
|
||||
$type_tabs = [
|
||||
'' => __('全部类型', 'aksrt'),
|
||||
'resource' => __('内容付费', 'aksrt'),
|
||||
'vip' => __('VIP', 'aksrt'),
|
||||
'recharge' => __('充值', 'aksrt'),
|
||||
'shop_product' => __('商城商品', 'aksrt'),
|
||||
];
|
||||
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
||||
$tab_bits = [];
|
||||
foreach ($type_tabs as $tkey => $tlabel) {
|
||||
$url = $this->page_url('aksrt_orders', array_filter([
|
||||
'product_type' => $tkey !== '' ? $tkey : null,
|
||||
'product_id' => $product_id > 0 ? $product_id : null,
|
||||
'user_id' => $user_id > 0 ? $user_id : null,
|
||||
'payment_method' => $pay_method !== '' ? $pay_method : null,
|
||||
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
||||
]));
|
||||
$cls = ($product_type === $tkey) ? 'current' : '';
|
||||
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
||||
}
|
||||
echo implode(' | ', $tab_bits);
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
|
||||
// Payment method quick tabs.
|
||||
$method_tabs = [
|
||||
'' => __('全部支付', 'aksrt'),
|
||||
'balance' => __('余额', 'aksrt'),
|
||||
'points' => __('积分', 'aksrt'),
|
||||
'alipay' => __('支付宝', 'aksrt'),
|
||||
'wechat' => __('微信', 'aksrt'),
|
||||
];
|
||||
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
||||
$m_bits = [];
|
||||
foreach ($method_tabs as $mkey => $mlabel) {
|
||||
$url = $this->page_url('aksrt_orders', array_filter([
|
||||
'product_type' => $product_type !== '' ? $product_type : null,
|
||||
'product_id' => $product_id > 0 ? $product_id : null,
|
||||
'user_id' => $user_id > 0 ? $user_id : null,
|
||||
'payment_method' => $mkey !== '' ? $mkey : null,
|
||||
'status' => ($status !== '' && $status !== 'all') ? $status : null,
|
||||
]));
|
||||
$cls = ($pay_method === $mkey) ? 'current' : '';
|
||||
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
||||
}
|
||||
echo implode(' | ', $m_bits);
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_orders" />';
|
||||
if ($product_type !== '') {
|
||||
echo '<input type="hidden" name="product_type" value="' . esc_attr($product_type) . '" />';
|
||||
}
|
||||
if ($product_id > 0) {
|
||||
echo '<input type="hidden" name="product_id" value="' . esc_attr((string) $product_id) . '" />';
|
||||
}
|
||||
if ($pay_method !== '') {
|
||||
echo '<input type="hidden" name="payment_method" value="' . esc_attr($pay_method) . '" />';
|
||||
}
|
||||
echo '<select name="status">';
|
||||
echo '<option value="">' . esc_html__('全部状态', 'aksrt') . '</option>';
|
||||
foreach ($labels as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($status, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号 / 类型 / 用户', 'aksrt') . '" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
if ($product_id > 0 || $product_type !== '' || $user_id > 0 || $pay_method !== '') {
|
||||
$filter_bits = [];
|
||||
if ($product_type !== '') {
|
||||
$filter_bits[] = sprintf(__('类型=%s', 'aksrt'), $product_type);
|
||||
}
|
||||
if ($product_id > 0) {
|
||||
$filter_bits[] = sprintf(__('商品ID=%d', 'aksrt'), $product_id);
|
||||
}
|
||||
if ($user_id > 0) {
|
||||
$filter_bits[] = sprintf(__('用户=%d', 'aksrt'), $user_id);
|
||||
}
|
||||
if ($pay_method !== '') {
|
||||
$filter_bits[] = sprintf(__('支付=%s', 'aksrt'), $pay_method);
|
||||
}
|
||||
echo ' <span class="description">' . esc_html(implode(' · ', $filter_bits)) . '</span>';
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders')) . '">' . esc_html__('清除过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
|
||||
// Hygiene actions (zibll clear_order / delete).
|
||||
echo '<p style="margin:8px 0 12px;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭/已过期订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_clear_closed_orders" />';
|
||||
echo '<input type="hidden" name="days" value="14" />';
|
||||
wp_nonce_field('aksrt_pay_clear_closed_orders');
|
||||
submit_button(__('清理 14 天前关闭订单', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
echo '</p>';
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('类型', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('金额', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('状态', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('时间', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无订单', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$order_number = (string) ($row['order_number'] ?? '');
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$status_key = (string) ($row['status'] ?? '');
|
||||
$status_label = $labels[$status_key] ?? $status_key;
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td><code>' . esc_html($order_number) . '</code></td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['product_type'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($status_label) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
||||
$this->render_order_quick_status_buttons($order_number, $status_key);
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_order_status_' . $order_number);
|
||||
echo '<select name="new_status">';
|
||||
foreach ($labels as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($status_key, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select>';
|
||||
submit_button(__('更新', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
if (in_array($status_key, ['cancelled', 'expired'], true)) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_delete_order" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_delete_order_' . $order_number);
|
||||
submit_button(__('删除', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
private function render_order_detail_page(string $order_number): void {
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$order = $repo->get($order_number);
|
||||
if (!$order) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('订单不存在。', 'aksrt') . '</p></div>';
|
||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$labels = class_exists('\AKSRT\Core\DomainModel')
|
||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||
: [];
|
||||
$context = $repo->get_admin_order_context($order_number);
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$customer_note = (string) ($context['customer_note'] ?? $order['note_text'] ?? '');
|
||||
$admin_note = is_array($meta) ? (string) ($meta['admin_note'] ?? '') : '';
|
||||
if ($admin_note === '' && $customer_note === '' && !empty($order['note']) && !is_array(json_decode((string) $order['note'], true))) {
|
||||
$admin_note = (string) $order['note'];
|
||||
}
|
||||
|
||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
|
||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||
$rows = [
|
||||
__('订单号', 'aksrt') => $order_number,
|
||||
__('用户', 'aksrt') => $user ? $user->display_name . ' (#' . $user_id . ')' : (string) $user_id,
|
||||
__('商品类型', 'aksrt') => (string) ($order['product_type'] ?? ''),
|
||||
__('商品 ID', 'aksrt') => (string) ($order['product_id'] ?? ''),
|
||||
__('金额', 'aksrt') => number_format((float) ($order['amount'] ?? 0), 2) . ' ' . (string) ($order['currency'] ?? ''),
|
||||
__('状态', 'aksrt') => $labels[$status] ?? $status,
|
||||
__('支付方式', 'aksrt') => (string) ($order['payment_method'] ?? ''),
|
||||
__('交易号', 'aksrt') => (string) ($order['payment_transaction_id'] ?? ''),
|
||||
__('创建时间', 'aksrt') => (string) ($order['created_at'] ?? ''),
|
||||
__('支付时间', 'aksrt') => (string) ($order['paid_at'] ?? ''),
|
||||
__('买家备注', 'aksrt') => $customer_note,
|
||||
];
|
||||
foreach ($rows as $label => $value) {
|
||||
echo '<tr><th style="width:160px;">' . esc_html($label) . '</th><td>' . esc_html((string) $value) . '</td></tr>';
|
||||
}
|
||||
if (!empty($context)) {
|
||||
echo '<tr><th>' . esc_html__('附加上下文', 'aksrt') . '</th><td><pre style="white-space:pre-wrap;margin:0;">' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
|
||||
echo '<h2 style="margin-top:20px;">' . esc_html__('运营备注', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="max-width:960px;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_note" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_order_note_' . $order_number);
|
||||
echo '<p><textarea name="admin_note" rows="3" class="large-text">' . esc_textarea($admin_note) . '</textarea></p>';
|
||||
submit_button(__('保存备注', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
echo '<h2 style="margin-top:20px;">' . esc_html__('更新状态', 'aksrt') . '</h2>';
|
||||
echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;">';
|
||||
$this->render_order_quick_status_buttons($order_number, $status, true);
|
||||
echo '</div>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:8px;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
echo '<input type="hidden" name="redirect_order" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_order_status_' . $order_number);
|
||||
echo '<select name="new_status">';
|
||||
foreach ($labels as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
submit_button(__('保存状态', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
public function handle_update_order_status(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
|
||||
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
|
||||
check_admin_referer('aksrt_pay_update_order_status_' . $order_number);
|
||||
|
||||
$ok = false;
|
||||
if ($order_number !== '' && $new_status !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status, ['force' => true]);
|
||||
}
|
||||
|
||||
$args = ['aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed'];
|
||||
$redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : '';
|
||||
if ($redirect_order !== '') {
|
||||
$args['order'] = $redirect_order;
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', $args));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_update_order_note(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
|
||||
$admin_note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : '';
|
||||
check_admin_referer('aksrt_pay_update_order_note_' . $order_number);
|
||||
|
||||
$ok = false;
|
||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->update_admin_note($order_number, $admin_note);
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', [
|
||||
'order' => $order_number,
|
||||
'aksrt_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed',
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_clear_closed_orders(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_pay_clear_closed_orders');
|
||||
$days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14;
|
||||
$deleted = 0;
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$deleted = \AKSRT\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
|
||||
}
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', [
|
||||
'aksrt_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
|
||||
'cleared' => $deleted,
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_delete_order(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
|
||||
check_admin_referer('aksrt_pay_delete_order_' . $order_number);
|
||||
$ok = false;
|
||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$status = is_array($order) ? (string) ($order['status'] ?? '') : '';
|
||||
// Only allow hard-delete of closed/expired rows (safety).
|
||||
if (in_array($status, ['cancelled', 'expired'], true)) {
|
||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->delete_order($order_number);
|
||||
}
|
||||
}
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', [
|
||||
'aksrt_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed',
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void {
|
||||
$actions = [
|
||||
'completed' => __('完成', 'aksrt'),
|
||||
'refunded' => __('退款', 'aksrt'),
|
||||
'cancelled' => __('关闭', 'aksrt'),
|
||||
];
|
||||
foreach ($actions as $status => $label) {
|
||||
if ($status === $current_status) {
|
||||
continue;
|
||||
}
|
||||
$confirm = '';
|
||||
if ($status === 'refunded') {
|
||||
$confirm = __('确认将该订单标记为已退款?', 'aksrt');
|
||||
} elseif ($status === 'cancelled') {
|
||||
$confirm = __('确认关闭该订单?', 'aksrt');
|
||||
} elseif ($status === 'completed') {
|
||||
$confirm = __('确认完成该订单?', 'aksrt');
|
||||
}
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_status" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
echo '<input type="hidden" name="new_status" value="' . esc_attr($status) . '" />';
|
||||
if ($redirect_detail) {
|
||||
echo '<input type="hidden" name="redirect_order" value="' . esc_attr($order_number) . '" />';
|
||||
}
|
||||
wp_nonce_field('aksrt_pay_update_order_status_' . $order_number);
|
||||
printf(
|
||||
'<button type="submit" class="button button-small"%s>%s</button>',
|
||||
$confirm !== '' ? ' onclick="return confirm(\'' . esc_js($confirm) . '\');"' : '',
|
||||
esc_html($label)
|
||||
);
|
||||
echo '</form>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'order_updated' => __('订单状态已更新。', 'aksrt'),
|
||||
'order_failed' => __('订单状态更新失败。', 'aksrt'),
|
||||
'order_note_updated' => __('订单备注已保存。', 'aksrt'),
|
||||
'order_note_failed' => __('订单备注保存失败。', 'aksrt'),
|
||||
'orders_cleared' => sprintf(
|
||||
/* translators: %d deleted count */
|
||||
__('已清理 %d 条关闭订单。', 'aksrt'),
|
||||
max(0, (int) ($_GET['cleared'] ?? 0))
|
||||
),
|
||||
'orders_clear_empty' => __('没有可清理的关闭订单。', 'aksrt'),
|
||||
'order_deleted' => __('订单已删除。', 'aksrt'),
|
||||
'order_delete_failed' => __('订单删除失败(仅允许删除已关闭/过期订单)。', 'aksrt'),
|
||||
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
|
||||
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
|
||||
'shipping_created' => __('发货记录已创建。', 'aksrt'),
|
||||
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
|
||||
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
|
||||
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayProducts {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_pay_products';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Read-only page; menu callback only.
|
||||
}
|
||||
|
||||
public function render_products_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('付费文章', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Core\DomainModel')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
|
||||
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
|
||||
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||
|
||||
$query = $this->query_paid_posts([
|
||||
'pay_type' => $pay_type,
|
||||
'search' => $search,
|
||||
'page' => $page_num,
|
||||
'per_page' => $per_page,
|
||||
]);
|
||||
|
||||
$posts = $query['posts'];
|
||||
$total = $query['total'];
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
|
||||
$sales_map = [];
|
||||
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$ids = array_map(static function ($p) {
|
||||
return (int) $p->ID;
|
||||
}, $posts);
|
||||
$sales_map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
|
||||
$ids,
|
||||
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||
);
|
||||
}
|
||||
|
||||
$this->render_filters($pay_type, $search, $type_labels);
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th style="width:28%;">' . esc_html__('商品', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('ID', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('付费类型', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('模式', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('标价', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('销售均价', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('订单销量', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('Meta 销量', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('销售总额', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$posts) {
|
||||
echo '<tr><td colspan="10">' . esc_html__('暂无付费文章。', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$post_id = (int) $post->ID;
|
||||
$meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
|
||||
$pt = \AKSRT\Core\DomainModel::normalize_pay_type((string) ($meta['pay_type'] ?? \AKSRT\Core\DomainModel::PAY_TYPE_NO));
|
||||
$pt_label = $type_labels[$pt] ?? $pt;
|
||||
$modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
|
||||
$modo_label = $modo === 'points' ? __('积分', 'aksrt') : __('现金', 'aksrt');
|
||||
|
||||
if ($modo === 'points') {
|
||||
$list_price = (int) ceil((float) ($meta['points_price'] ?? $meta['price'] ?? 0));
|
||||
$price_text = sprintf(__('%d 积分', 'aksrt'), $list_price);
|
||||
} else {
|
||||
$list_price = round((float) ($meta['price'] ?? 0), 2);
|
||||
$price_text = number_format($list_price, 2);
|
||||
}
|
||||
|
||||
$sales = $sales_map[$post_id] ?? ['sales_count' => 0, 'avg_amount' => 0.0, 'revenue' => 0.0];
|
||||
$db_sales = (int) ($sales['sales_count'] ?? 0);
|
||||
$avg = (float) ($sales['avg_amount'] ?? 0);
|
||||
$revenue = (float) ($sales['revenue'] ?? 0);
|
||||
$meta_sales = (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true);
|
||||
|
||||
$edit_url = get_edit_post_link($post_id, 'raw');
|
||||
$title = get_the_title($post_id);
|
||||
if ($title === '') {
|
||||
$title = sprintf(__('(无标题 #%d)', 'aksrt'), $post_id);
|
||||
}
|
||||
|
||||
$orders_url = $this->page_url('aksrt_orders', [
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $post_id,
|
||||
]);
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td>';
|
||||
if ($edit_url) {
|
||||
echo '<a href="' . esc_url($edit_url) . '"><strong>' . esc_html($title) . '</strong></a>';
|
||||
} else {
|
||||
echo '<strong>' . esc_html($title) . '</strong>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '<td>' . esc_html((string) $post_id) . '</td>';
|
||||
echo '<td>' . esc_html((string) $pt_label) . '</td>';
|
||||
echo '<td>' . esc_html($modo_label) . '</td>';
|
||||
echo '<td>' . esc_html($price_text) . '</td>';
|
||||
echo '<td>' . esc_html(number_format($avg, 2)) . '</td>';
|
||||
echo '<td>' . esc_html((string) $db_sales) . '</td>';
|
||||
echo '<td>' . esc_html((string) max(0, $meta_sales)) . '</td>';
|
||||
echo '<td>' . esc_html(number_format($revenue, 2)) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;">';
|
||||
if ($edit_url) {
|
||||
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '<a class="button button-small" href="' . esc_url($orders_url) . '">' . esc_html__('订单', 'aksrt') . '</a>';
|
||||
if ($pt === \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
|
||||
$dl_url = $this->page_url('aksrt_download_logs', ['post_id' => $post_id]);
|
||||
echo '<a class="button button-small" href="' . esc_url($dl_url) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
|
||||
}
|
||||
$permalink = get_permalink($post_id);
|
||||
if ($permalink) {
|
||||
echo '<a class="button button-small" target="_blank" rel="noopener noreferrer" href="' . esc_url($permalink) . '">' . esc_html__('查看', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{pay_type?:string,search?:string,page?:int,per_page?:int} $args
|
||||
* @return array{posts: \WP_Post[], total: int}
|
||||
*/
|
||||
private function query_paid_posts(array $args): array {
|
||||
$pay_type = sanitize_key((string) ($args['pay_type'] ?? ''));
|
||||
$search = (string) ($args['search'] ?? '');
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = max(1, min(100, (int) ($args['per_page'] ?? 20)));
|
||||
|
||||
$meta_query = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_META_KEY,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
];
|
||||
|
||||
// Prefer denormalized type key when present; still exclude "no".
|
||||
if ($pay_type !== '' && $pay_type !== \AKSRT\Core\DomainModel::PAY_TYPE_NO) {
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
|
||||
'value' => $pay_type,
|
||||
'compare' => '=',
|
||||
],
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_META_KEY,
|
||||
'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"',
|
||||
'compare' => 'LIKE',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
|
||||
'value' => \AKSRT\Core\DomainModel::PAY_TYPE_NO,
|
||||
'compare' => '!=',
|
||||
],
|
||||
[
|
||||
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$q_args = [
|
||||
'post_type' => 'post',
|
||||
'post_status' => ['publish', 'draft', 'pending', 'private', 'future'],
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $page,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'meta_query' => $meta_query,
|
||||
'fields' => 'all',
|
||||
'no_found_rows' => false,
|
||||
'update_post_meta_cache' => true,
|
||||
'update_post_term_cache' => false,
|
||||
];
|
||||
|
||||
if ($search !== '') {
|
||||
if (ctype_digit($search)) {
|
||||
$q_args['post__in'] = [(int) $search];
|
||||
} else {
|
||||
$q_args['s'] = $search;
|
||||
}
|
||||
}
|
||||
|
||||
$q = new \WP_Query($q_args);
|
||||
$posts = [];
|
||||
foreach ($q->posts as $post) {
|
||||
if (!($post instanceof \WP_Post)) {
|
||||
continue;
|
||||
}
|
||||
// Drop posts whose pay bag is effectively free/no.
|
||||
if (function_exists('aksrt_is_paid_post') && !aksrt_is_paid_post($post)) {
|
||||
continue;
|
||||
}
|
||||
if ($pay_type !== '' && function_exists('aksrt_get_pay_meta')) {
|
||||
$meta = aksrt_get_pay_meta($post);
|
||||
$pt = \AKSRT\Core\DomainModel::normalize_pay_type((string) ($meta['pay_type'] ?? ''));
|
||||
if ($pt !== $pay_type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$posts[] = $post;
|
||||
}
|
||||
|
||||
// When post-filtering drops rows, fall back to order-driven list if empty page
|
||||
// so operators still see historical sold posts without pay meta.
|
||||
if (empty($posts) && $search === '' && $pay_type === '' && $page === 1) {
|
||||
$order_posts = $this->posts_from_order_summary($per_page, 0);
|
||||
if (!empty($order_posts['posts'])) {
|
||||
return $order_posts;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'posts' => $posts,
|
||||
'total' => (int) $q->found_posts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: posts that already have paid resource orders.
|
||||
*
|
||||
* @return array{posts: \WP_Post[], total: int}
|
||||
*/
|
||||
private function posts_from_order_summary(int $limit, int $offset): array {
|
||||
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
return ['posts' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
$summary = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_summary([
|
||||
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'orderby' => 'sales_count',
|
||||
'order' => 'DESC',
|
||||
]);
|
||||
|
||||
$posts = [];
|
||||
foreach ($summary['items'] as $item) {
|
||||
$pid = (int) ($item['product_id'] ?? 0);
|
||||
$post = $pid > 0 ? get_post($pid) : null;
|
||||
if ($post instanceof \WP_Post && $post->post_type === 'post') {
|
||||
$posts[] = $post;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'posts' => $posts,
|
||||
'total' => (int) ($summary['total'] ?? count($posts)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $type_labels
|
||||
*/
|
||||
private function render_filters(string $pay_type, string $search, array $type_labels): void {
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
echo '<select name="pay_type">';
|
||||
echo '<option value="">' . esc_html__('全部付费类型', 'aksrt') . '</option>';
|
||||
foreach ($type_labels as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($pay_type, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('标题或文章 ID', 'aksrt') . '" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
echo '<ul class="subsubsub" style="margin:0 0 12px;float:none;">';
|
||||
$base = $this->page_url(self::PAGE_SLUG);
|
||||
$all_class = $pay_type === '' ? 'current' : '';
|
||||
echo '<li><a class="' . esc_attr($all_class) . '" href="' . esc_url($base) . '">' . esc_html__('全部', 'aksrt') . '</a></li>';
|
||||
foreach ($type_labels as $key => $label) {
|
||||
$url = $this->page_url(self::PAGE_SLUG, array_filter([
|
||||
'pay_type' => $key,
|
||||
's' => $search !== '' ? $search : null,
|
||||
]));
|
||||
$cls = $pay_type === $key ? 'current' : '';
|
||||
echo ' | <li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a></li>';
|
||||
}
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
echo '<p class="description">' . esc_html__('本页统计内容付费文章(product_type=resource),不含商城实体商品。订单销量来自已支付/已发货/已完成订单;Meta 销量为缓存计数。', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayRebate {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_rebate';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_settle_rebate', [$this, 'handle_settle_rebate']);
|
||||
add_action('admin_post_aksrt_pay_cancel_rebate', [$this, 'handle_cancel_rebate']);
|
||||
}
|
||||
|
||||
public function render_rebate_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('推广返佣', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('返佣模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : (isset($_GET['referrer_id']) ? max(0, (int) $_GET['referrer_id']) : 0);
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\Rebate::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: settled amount */
|
||||
__('记录 %1$d · 待结算 %2$d · 已结算金额 %3$s', 'aksrt'),
|
||||
(int) ($stats['total'] ?? 0),
|
||||
(int) ($stats['pending'] ?? 0),
|
||||
number_format((float) ($stats['settled_amount'] ?? 0), 2)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_rebate" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('推广用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
if ($user_id > 0) {
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_rebate')) . '">' . esc_html__('清除用户过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
|
||||
$list_args = [
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
];
|
||||
if ($user_id > 0) {
|
||||
$list_args['user_id'] = $user_id;
|
||||
}
|
||||
$result = $svc->get_list($list_args);
|
||||
$items = $result['items'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无返佣记录', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:4px;">';
|
||||
if ($st === 'pending') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_settle_rebate" />';
|
||||
echo '<input type="hidden" name="rebate_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_settle_rebate_' . $id);
|
||||
submit_button(__('结算', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_cancel_rebate" />';
|
||||
echo '<input type="hidden" name="rebate_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_cancel_rebate_' . $id);
|
||||
submit_button(__('取消', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
public function handle_settle_rebate(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['rebate_id']) ? (int) $_POST['rebate_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_settle_rebate_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Rebate') && \AKSRT\Payment\Rebate::get_instance()->settle($id);
|
||||
wp_safe_redirect($this->page_url('aksrt_rebate', ['aksrt_pay_msg' => $ok ? 'rebate_settled' : 'rebate_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_cancel_rebate(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['rebate_id']) ? (int) $_POST['rebate_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_cancel_rebate_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Rebate') && \AKSRT\Payment\Rebate::get_instance()->cancel($id);
|
||||
wp_safe_redirect($this->page_url('aksrt_rebate', ['aksrt_pay_msg' => $ok ? 'rebate_cancelled' : 'rebate_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'rebate_settled' => __('返佣已结算。', 'aksrt'),
|
||||
'rebate_cancelled' => __('返佣已取消。', 'aksrt'),
|
||||
'rebate_failed' => __('返佣操作失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayWithdrawals {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_withdraw';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_update_withdrawal', [$this, 'handle_update_withdrawal']);
|
||||
}
|
||||
|
||||
public function render_withdrawals_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('提现审核', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('提现模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: completed 4: paid */
|
||||
__('共 %1$d 条 · 待处理 %2$d · 已完成 %3$d · 已打款 %4$s', 'aksrt'),
|
||||
(int) ($stats['total'] ?? 0),
|
||||
(int) ($stats['pending'] ?? 0),
|
||||
(int) ($stats['completed'] ?? 0),
|
||||
number_format((float) ($stats['total_paid'] ?? 0), 2)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_withdraw" />';
|
||||
echo '<select name="status">';
|
||||
$status_options = [
|
||||
'' => __('全部状态', 'aksrt'),
|
||||
'pending' => __('待处理', 'aksrt'),
|
||||
'approved' => __('已通过', 'aksrt'),
|
||||
'processing' => __('处理中', 'aksrt'),
|
||||
'completed' => __('已完成', 'aksrt'),
|
||||
'rejected' => __('已拒绝', 'aksrt'),
|
||||
];
|
||||
foreach ($status_options as $key => $label) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($status, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
$items = $svc->get_all([
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
]);
|
||||
$total = $svc->get_count(['status' => $status]);
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>ID</th>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('金额', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('方式', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('账户', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('状态', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('申请时间', 'aksrt') . '</th>';
|
||||
echo '<th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="8">' . esc_html__('暂无提现记录', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
$gross = (float) ($row['gross_amount'] ?? $row['amount'] ?? 0);
|
||||
$net = (float) ($row['amount'] ?? 0);
|
||||
$fee = (int) ($row['fee_rate'] ?? 0);
|
||||
$acct = (string) ($row['display_account'] ?? $row['account_info'] ?? '');
|
||||
$amount_txt = number_format($net, 2);
|
||||
if ($gross > 0 && abs($gross - $net) > 0.001) {
|
||||
$amount_txt = sprintf(
|
||||
/* translators: 1: gross 2: net 3: fee percent */
|
||||
__('申请 %1$s / 到账 %2$s(手续费 %3$d%%)', 'aksrt'),
|
||||
number_format($gross, 2),
|
||||
number_format($net, 2),
|
||||
$fee
|
||||
);
|
||||
}
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td>' . esc_html($amount_txt) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['method'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($acct) . '</td>';
|
||||
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_withdrawal" />';
|
||||
echo '<input type="hidden" name="withdrawal_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_withdrawal_' . $id);
|
||||
echo '<select name="new_status">';
|
||||
foreach ($status_options as $key => $label) {
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
selected($st, $key, false),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select>';
|
||||
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" value="" />';
|
||||
submit_button(__('更新', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
if ($pages > 1) {
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
public function handle_update_withdrawal(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$id = isset($_POST['withdrawal_id']) ? (int) $_POST['withdrawal_id'] : 0;
|
||||
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
check_admin_referer('aksrt_pay_update_withdrawal_' . $id);
|
||||
|
||||
$msg = 'withdraw_failed';
|
||||
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id, $new_status, $note);
|
||||
if (!is_wp_error($result) && $result) {
|
||||
$msg = 'withdraw_updated';
|
||||
} elseif (is_wp_error($result) && $result->get_error_code() === 'aksrt_balance') {
|
||||
$msg = 'withdraw_refund_failed';
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_withdraw', [
|
||||
'aksrt_pay_msg' => $msg,
|
||||
]));
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Income / rebate
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'withdraw_updated' => __('提现状态已更新。', 'aksrt'),
|
||||
'withdraw_failed' => __('提现状态更新失败。', 'aksrt'),
|
||||
'withdraw_refund_failed' => __('状态未更新:余额退回失败,请检查钱包后重试。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class UsersMenus {
|
||||
|
||||
use UsersMenusVerify;
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action('admin_menu', [$this, 'register_menus']);
|
||||
add_action('admin_notices', [$this, 'pending_notices']);
|
||||
add_action('admin_post_aksrt_users_send_message', [$this, 'handle_send_message']);
|
||||
add_action('admin_post_aksrt_users_review_verify', [$this, 'handle_review_verify']);
|
||||
add_action('admin_post_aksrt_users_delete_verify', [$this, 'handle_delete_verify']);
|
||||
add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']);
|
||||
add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']);
|
||||
add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']);
|
||||
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
|
||||
add_action('admin_post_aksrt_users_batch_invite_codes', [$this, 'handle_batch_invite_codes']);
|
||||
add_action('admin_post_aksrt_users_disable_invite_code', [$this, 'handle_disable_invite_code']);
|
||||
}
|
||||
|
||||
public function register_menus(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_submenu_page(
|
||||
'users.php',
|
||||
__('消息管理', 'aksrt'),
|
||||
__('消息管理', 'aksrt'),
|
||||
self::CAPABILITY,
|
||||
'aksrt_messages',
|
||||
[$this, 'render_messages_page']
|
||||
);
|
||||
add_submenu_page(
|
||||
'users.php',
|
||||
__('邀请码管理', 'aksrt'),
|
||||
__('邀请码管理', 'aksrt'),
|
||||
self::CAPABILITY,
|
||||
'aksrt_invite_codes',
|
||||
[$this, 'render_invite_codes_page']
|
||||
);
|
||||
add_submenu_page(
|
||||
'users.php',
|
||||
__('身份认证', 'aksrt'),
|
||||
__('身份认证', 'aksrt'),
|
||||
self::CAPABILITY,
|
||||
'aksrt_user_auth',
|
||||
[$this, 'render_verify_page']
|
||||
);
|
||||
add_submenu_page(
|
||||
'users.php',
|
||||
__('举报&禁封', 'aksrt'),
|
||||
__('举报&禁封', 'aksrt'),
|
||||
self::CAPABILITY,
|
||||
'aksrt_user_ban',
|
||||
[$this, 'render_ban_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function pending_notices(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
return;
|
||||
}
|
||||
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
|
||||
|
||||
if ($page !== 'aksrt_user_auth') {
|
||||
$pending_verify = $this->count_verify_status('pending');
|
||||
if ($pending_verify > 0) {
|
||||
$url = admin_url('users.php?page=aksrt_user_auth&status=pending');
|
||||
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('用户身份认证申请待处理', 'aksrt') . '</h3>';
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: %d count */
|
||||
__('您有 %d 个用户身份认证申请待处理', 'aksrt'),
|
||||
$pending_verify
|
||||
)) . '</p>';
|
||||
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
$pending_reports = (int) \AKSRT\Messages\ReportCenter::get_instance()->get_pending_count();
|
||||
if ($pending_reports > 0) {
|
||||
$url = admin_url('users.php?page=aksrt_user_ban&tab=reports&status=pending');
|
||||
echo '<div class="notice notice-info is-dismissible"><h3>' . esc_html__('举报待处理', 'aksrt') . '</h3>';
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: %d count */
|
||||
__('您有 %d 条举报待处理', 'aksrt'),
|
||||
$pending_reports
|
||||
)) . '</p>';
|
||||
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($page !== 'aksrt_user_ban' && class_exists('\AKSRT\User\BanAppeal')) {
|
||||
$pending_appeals = count(get_users([
|
||||
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
|
||||
'meta_value' => 'pending',
|
||||
'number' => 50,
|
||||
'fields' => 'ID',
|
||||
]));
|
||||
if ($pending_appeals > 0) {
|
||||
$url = admin_url('users.php?page=aksrt_user_ban&tab=appeals&status=pending');
|
||||
echo '<div class="notice notice-warning is-dismissible"><h3>' . esc_html__('禁封申诉待处理', 'aksrt') . '</h3>';
|
||||
echo '<p>' . esc_html(sprintf(__('您有 %d 条禁封申诉待处理', 'aksrt'), $pending_appeals)) . '</p>';
|
||||
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a></p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function flash(): void {
|
||||
if (empty($_GET['aksrt_users_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_users_msg']));
|
||||
$map = [
|
||||
'message_sent' => __('消息已发送。', 'aksrt'),
|
||||
'message_failed' => __('消息发送失败。', 'aksrt'),
|
||||
'verify_updated' => __('认证状态已更新。', 'aksrt'),
|
||||
'verify_deleted' => __('认证记录已删除。', 'aksrt'),
|
||||
'verify_failed' => __('认证审核失败。', 'aksrt'),
|
||||
'report_updated' => __('举报状态已更新。', 'aksrt'),
|
||||
'report_failed' => __('举报更新失败。', 'aksrt'),
|
||||
'restriction_added' => __('限制已添加。', 'aksrt'),
|
||||
'restriction_removed' => __('限制已移除。', 'aksrt'),
|
||||
'restriction_failed' => __('限制操作失败。', 'aksrt'),
|
||||
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
|
||||
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
|
||||
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
|
||||
'invite_batch_ok' => __('邀请码批量生成完成。', 'aksrt'),
|
||||
'invite_batch_failed' => __('邀请码批量生成失败。', 'aksrt'),
|
||||
'invite_disabled' => __('邀请码已禁用。', 'aksrt'),
|
||||
'invite_failed' => __('邀请码操作失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
$message = $map[$code];
|
||||
if ($code === 'verify_failed' && isset($_GET['aksrt_verify_error'])) {
|
||||
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_verify_error']));
|
||||
}
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
|
||||
}
|
||||
|
||||
private function redirect(string $page, string $msg, array $extra = []): void {
|
||||
wp_safe_redirect(add_query_arg(array_merge(['page' => $page, 'aksrt_users_msg' => $msg], $extra), admin_url('users.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Messages
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
public function render_messages_page(): void {
|
||||
$this->ensure_cap();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('消息管理', 'aksrt') . '</h1>';
|
||||
$this->flash();
|
||||
|
||||
echo '<h2>' . esc_html__('发送系统消息', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_send_message" />';
|
||||
wp_nonce_field('aksrt_users_send_message');
|
||||
echo '<p><label>' . esc_html__('用户 ID(0=广播给管理员自己测试)', 'aksrt') . '<br><input type="number" name="user_id" value="0" min="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('标题', 'aksrt') . '<br><input type="text" name="title" class="regular-text" required /></label></p>';
|
||||
echo '<p><label>' . esc_html__('内容', 'aksrt') . '<br><textarea name="content" class="large-text" rows="4" required></textarea></label></p>';
|
||||
submit_button(__('发送', 'aksrt'));
|
||||
echo '</form>';
|
||||
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 30;
|
||||
$offset = ($page_num - 1) * $per_page;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
|
||||
if ($table === '') {
|
||||
echo '<div class="notice notice-warning"><p>' . esc_html__('通知表不可用。', 'aksrt') . '</p></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
|
||||
$items = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$per_page,
|
||||
$offset
|
||||
), ARRAY_A);
|
||||
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
|
||||
|
||||
echo '<h2>' . esc_html__('最近消息', 'aksrt') . '</h2>';
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('标题', 'aksrt') . '</th><th>' . esc_html__('已读', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="6">' . esc_html__('暂无消息', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ((array) $items as $row) {
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) ($row['id'] ?? '')) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['type'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['title'] ?? $row['description'] ?? '')) . '</td>';
|
||||
echo '<td>' . (!empty($row['is_read']) ? esc_html__('是', 'aksrt') : esc_html__('否', 'aksrt')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->pagination($pages, $page_num);
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function handle_send_message(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_users_send_message');
|
||||
|
||||
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$title = isset($_POST['title']) ? sanitize_text_field((string) wp_unslash($_POST['title'])) : '';
|
||||
$content = isset($_POST['content']) ? sanitize_textarea_field((string) wp_unslash($_POST['content'])) : '';
|
||||
if ($user_id <= 0) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
|
||||
$ok = false;
|
||||
if ($content !== '' && class_exists('\AKSRT\Messages\Message')) {
|
||||
$ok = \AKSRT\Messages\Message::get_instance()->send($user_id, 'system', $content, ['title' => $title !== '' ? $title : $content]);
|
||||
}
|
||||
$this->redirect('aksrt_messages', $ok ? 'message_sent' : 'message_failed');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Invite codes
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
public function render_invite_codes_page(): void {
|
||||
$this->ensure_cap();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('邀请码管理', 'aksrt') . '</h1>';
|
||||
$this->flash();
|
||||
|
||||
echo '<h2>' . esc_html__('批量生成独立邀请码', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_batch_invite_codes" />';
|
||||
wp_nonce_field('aksrt_users_batch_invite_codes');
|
||||
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="100" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="4" max="12" value="6" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" value="" class="regular-text" /></label></p>';
|
||||
submit_button(__('生成', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
if (!empty($_GET['aksrt_generated'])) {
|
||||
echo '<div class="notice notice-success"><p>' . esc_html(sprintf(
|
||||
/* translators: %d count */
|
||||
__('成功生成 %d 个邀请码。', 'aksrt'),
|
||||
(int) $_GET['aksrt_generated']
|
||||
)) . '</p></div>';
|
||||
}
|
||||
|
||||
$standalone = get_option('aksrt_standalone_invite_codes', []);
|
||||
$standalone = is_array($standalone) ? array_reverse(array_values($standalone)) : [];
|
||||
|
||||
echo '<h2>' . esc_html__('独立邀请码', 'aksrt') . '</h2>';
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('批次', 'aksrt') . '</th><th>' . esc_html__('备注', 'aksrt') . '</th><th>' . esc_html__('创建时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$standalone) {
|
||||
echo '<tr><td colspan="6">' . esc_html__('暂无独立邀请码', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
$shown = 0;
|
||||
foreach ($standalone as $entry) {
|
||||
if ($shown >= 200) {
|
||||
break;
|
||||
}
|
||||
$code = sanitize_key((string) ($entry['code'] ?? ''));
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$disabled = !empty($entry['disabled']);
|
||||
$used = !empty($entry['used']) || !empty($entry['used_by']);
|
||||
$status = $disabled ? __('已禁用', 'aksrt') : ($used ? __('已使用', 'aksrt') : __('可用', 'aksrt'));
|
||||
echo '<tr>';
|
||||
echo '<td><code>' . esc_html($code) . '</code></td>';
|
||||
echo '<td>' . esc_html($status) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($entry['batch_id'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($entry['note'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($entry['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if (!$disabled && !$used) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_disable_invite_code" />';
|
||||
echo '<input type="hidden" name="code" value="' . esc_attr($code) . '" />';
|
||||
wp_nonce_field('aksrt_users_disable_invite_code_' . $code);
|
||||
submit_button(__('禁用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
$shown++;
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC LIMIT 200",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
echo '<h2>' . esc_html__('用户邀请码', 'aksrt') . '</h2>';
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('邀请人数', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$rows) {
|
||||
echo '<tr><td colspan="3">' . esc_html__('暂无用户邀请码', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
|
||||
foreach ((array) $rows as $row) {
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$count = 0;
|
||||
if ($invite_table) {
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$invite_table} WHERE inviter_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
echo '<tr>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['invite_code'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html((string) $count) . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table></div>';
|
||||
}
|
||||
|
||||
public function handle_batch_invite_codes(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_users_batch_invite_codes');
|
||||
|
||||
$count = isset($_POST['count']) ? max(1, min(100, (int) $_POST['count'])) : 10;
|
||||
$length = isset($_POST['length']) ? max(4, min(12, (int) $_POST['length'])) : 6;
|
||||
$prefix = isset($_POST['prefix']) ? sanitize_key((string) wp_unslash($_POST['prefix'])) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
$batch = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
|
||||
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
||||
|
||||
$codes = get_option('aksrt_standalone_invite_codes', []);
|
||||
$codes = is_array($codes) ? array_values($codes) : [];
|
||||
$existing = [];
|
||||
foreach ($codes as $entry) {
|
||||
$c = sanitize_key((string) ($entry['code'] ?? ''));
|
||||
if ($c !== '') {
|
||||
$existing[$c] = true;
|
||||
}
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$generated = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = '';
|
||||
for ($attempt = 0; $attempt < 20; $attempt++) {
|
||||
$candidate = $prefix;
|
||||
for ($j = 0; $j < $length; $j++) {
|
||||
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
$candidate = sanitize_key($candidate);
|
||||
if ($candidate === '' || isset($existing[$candidate])) {
|
||||
continue;
|
||||
}
|
||||
$user_hit = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
|
||||
$candidate
|
||||
));
|
||||
if ($user_hit) {
|
||||
continue;
|
||||
}
|
||||
$code = $candidate;
|
||||
break;
|
||||
}
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$existing[$code] = true;
|
||||
$codes[] = [
|
||||
'code' => $code,
|
||||
'created_at' => current_time('mysql'),
|
||||
'used' => false,
|
||||
'used_by' => 0,
|
||||
'used_at' => '',
|
||||
'disabled' => false,
|
||||
'note' => $note,
|
||||
'batch_id' => $batch,
|
||||
'created_by' => get_current_user_id(),
|
||||
];
|
||||
$generated[] = $code;
|
||||
}
|
||||
|
||||
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
|
||||
$ok = !empty($generated);
|
||||
$this->redirect('aksrt_invite_codes', $ok ? 'invite_batch_ok' : 'invite_batch_failed', [
|
||||
'aksrt_generated' => count($generated),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle_disable_invite_code(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$code = isset($_POST['code']) ? sanitize_key((string) wp_unslash($_POST['code'])) : '';
|
||||
check_admin_referer('aksrt_users_disable_invite_code_' . $code);
|
||||
|
||||
$ok = false;
|
||||
if ($code !== '') {
|
||||
$codes = get_option('aksrt_standalone_invite_codes', []);
|
||||
$codes = is_array($codes) ? $codes : [];
|
||||
foreach ($codes as $i => $entry) {
|
||||
if (sanitize_key((string) ($entry['code'] ?? '')) !== $code) {
|
||||
continue;
|
||||
}
|
||||
$entry['disabled'] = true;
|
||||
$codes[$i] = $entry;
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
if ($ok) {
|
||||
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
|
||||
}
|
||||
}
|
||||
$this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Ban / reports
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
public function render_ban_page(): void {
|
||||
$this->ensure_cap();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('举报&禁封', 'aksrt') . '</h1>';
|
||||
$this->flash();
|
||||
|
||||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'reports';
|
||||
echo '<h2 class="nav-tab-wrapper">';
|
||||
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'reports' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=reports')), esc_html__('举报', 'aksrt'));
|
||||
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'restrictions' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=restrictions')), esc_html__('禁封限制', 'aksrt'));
|
||||
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'appeals' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=appeals')), esc_html__('禁封申诉', 'aksrt'));
|
||||
echo '</h2>';
|
||||
|
||||
if ($tab === 'restrictions') {
|
||||
$this->render_restrictions_tab();
|
||||
} elseif ($tab === 'appeals') {
|
||||
$this->render_appeals_tab();
|
||||
} else {
|
||||
$this->render_reports_tab();
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_reports_tab(): void {
|
||||
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('举报中心未初始化。', 'aksrt') . '</p></div>';
|
||||
return;
|
||||
}
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
|
||||
$svc = \AKSRT\Messages\ReportCenter::get_instance();
|
||||
$counts = $svc->get_status_counts();
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1 pending 2 total */
|
||||
__('待处理 %1$d / 共 %2$d', 'aksrt'),
|
||||
(int) ($counts['pending'] ?? 0),
|
||||
(int) ($counts['total'] ?? 0)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="reports" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['pending' => __('待处理', 'aksrt'), 'reviewing' => __('审核中', 'aksrt'), 'resolved' => __('已处理', 'aksrt'), 'dismissed' => __('已驳回', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
$items = $svc->get_reports(['status' => $status, 'limit' => 50, 'offset' => 0]);
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('举报人', 'aksrt') . '</th><th>' . esc_html__('目标', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无举报', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
$rid = (int) ($row['reporter_id'] ?? 0);
|
||||
$ru = $rid > 0 ? get_userdata($rid) : null;
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($ru ? esc_html($ru->display_name) : esc_html((string) $rid)) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['target_type'] ?? '') . ' #' . (string) ($row['target_id'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['reason'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_update_report" />';
|
||||
echo '<input type="hidden" name="report_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_users_update_report_' . $id);
|
||||
echo '<select name="new_status">';
|
||||
foreach (['pending', 'reviewing', 'resolved', 'dismissed'] as $key) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($key));
|
||||
}
|
||||
echo '</select>';
|
||||
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
|
||||
submit_button(__('更新', 'aksrt'), 'small', '', false);
|
||||
echo '</form></td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function render_restrictions_tab(): void {
|
||||
if (!class_exists('\AKSRT\User\Restriction')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('限制模块未初始化。', 'aksrt') . '</p></div>';
|
||||
return;
|
||||
}
|
||||
$svc = \AKSRT\User\Restriction::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1 total 2 active 3 banned */
|
||||
__('限制记录 %1$d · 生效中 %2$d · 封禁中 %3$d', 'aksrt'),
|
||||
(int) ($stats['total'] ?? 0),
|
||||
(int) ($stats['active'] ?? 0),
|
||||
(int) ($stats['banned'] ?? 0)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<h2>' . esc_html__('添加限制', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_add_restriction" />';
|
||||
wp_nonce_field('aksrt_users_add_restriction');
|
||||
echo '<p><label>' . esc_html__('用户 ID', 'aksrt') . '<br><input type="number" name="user_id" min="1" required /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
|
||||
foreach (['warn' => __('警告', 'aksrt'), 'mute' => __('禁言', 'aksrt'), 'suspend' => __('停权', 'aksrt'), 'ban' => __('封禁', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s">%s</option>', esc_attr($key), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('范围', 'aksrt') . '<br><select name="scope">';
|
||||
foreach (['global' => __('全局', 'aksrt'), 'comment' => __('评论', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s">%s</option>', esc_attr($key), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('原因', 'aksrt') . '<br><input type="text" name="reason" class="regular-text" required /></label></p>';
|
||||
submit_button(__('添加', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('user_restriction') : '';
|
||||
if ($table === '') {
|
||||
echo '<div class="notice notice-warning"><p>' . esc_html__('限制表不可用。', 'aksrt') . '</p></div>';
|
||||
return;
|
||||
}
|
||||
$items = $wpdb->get_results("SELECT * FROM {$table} ORDER BY id DESC LIMIT 100", ARRAY_A);
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无限制记录', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ((array) $items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['type'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['scope'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['reason'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '—')) . '</td>';
|
||||
echo '<td><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_remove_restriction" />';
|
||||
echo '<input type="hidden" name="restriction_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_users_remove_restriction_' . $id);
|
||||
submit_button(__('移除', 'aksrt'), 'small', '', false);
|
||||
echo '</form></td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
public function handle_update_report(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['report_id']) ? (int) $_POST['report_id'] : 0;
|
||||
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
check_admin_referer('aksrt_users_update_report_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Messages\ReportCenter') && \AKSRT\Messages\ReportCenter::get_instance()->update_status($id, $new_status, $note);
|
||||
$this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'reports']);
|
||||
}
|
||||
|
||||
public function handle_add_restriction(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_users_add_restriction');
|
||||
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'warn';
|
||||
$scope = isset($_POST['scope']) ? sanitize_key((string) wp_unslash($_POST['scope'])) : 'global';
|
||||
$reason = isset($_POST['reason']) ? sanitize_text_field((string) wp_unslash($_POST['reason'])) : '';
|
||||
$ok = false;
|
||||
if ($user_id > 0 && $reason !== '' && class_exists('\AKSRT\User\Restriction')) {
|
||||
$result = \AKSRT\User\Restriction::get_instance()->restrict($user_id, $type, $reason, $scope, '', get_current_user_id());
|
||||
$ok = !empty($result['id']) || !empty($result['success']) || is_array($result);
|
||||
}
|
||||
$this->redirect('aksrt_user_ban', $ok ? 'restriction_added' : 'restriction_failed', ['tab' => 'restrictions']);
|
||||
}
|
||||
|
||||
public function handle_remove_restriction(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['restriction_id']) ? (int) $_POST['restriction_id'] : 0;
|
||||
check_admin_referer('aksrt_users_remove_restriction_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\User\Restriction') && \AKSRT\User\Restriction::get_instance()->remove_restriction($id);
|
||||
$this->redirect('aksrt_user_ban', $ok ? 'restriction_removed' : 'restriction_failed', ['tab' => 'restrictions']);
|
||||
}
|
||||
|
||||
|
||||
private function render_appeals_tab(): void {
|
||||
if (!class_exists('\AKSRT\User\BanAppeal')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('申诉模块未初始化。', 'aksrt') . '</p></div>';
|
||||
return;
|
||||
}
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
|
||||
if (!in_array($status, ['pending', 'approved', 'rejected', 'all'], true)) {
|
||||
$status = 'pending';
|
||||
}
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="appeals" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['pending' => __('待处理', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'all' => __('全部', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
$users = get_users([
|
||||
'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
|
||||
'meta_compare' => 'EXISTS',
|
||||
'number' => 100,
|
||||
'fields' => ['ID', 'display_name', 'user_login', 'user_email'],
|
||||
'orderby' => 'ID',
|
||||
'order' => 'DESC',
|
||||
]);
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('说明', 'aksrt') . '</th><th>' . esc_html__('提交时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
$shown = 0;
|
||||
foreach ($users as $u) {
|
||||
$uid = (int) $u->ID;
|
||||
$st = \AKSRT\User\BanAppeal::get_status($uid);
|
||||
if ($st === '') {
|
||||
continue;
|
||||
}
|
||||
if ($status !== 'all' && $st !== $status) {
|
||||
continue;
|
||||
}
|
||||
$payload = \AKSRT\User\BanAppeal::get_payload($uid);
|
||||
$shown++;
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $uid) . '</td>';
|
||||
echo '<td>' . esc_html($u->display_name . ' (' . $u->user_login . ')') . '<br><span class="description">' . esc_html($u->user_email) . '</span></td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($payload['reason_key'] ?? '—')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($payload['reason'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($payload['time'] ?? '—')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($st === 'pending') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_process_appeal" />';
|
||||
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
|
||||
wp_nonce_field('aksrt_users_process_appeal_' . $uid);
|
||||
echo '<input type="text" name="note" placeholder="' . esc_attr__('处理备注', 'aksrt') . '" class="regular-text" />';
|
||||
echo '<button type="submit" name="decision" value="approve" class="button button-primary">' . esc_html__('通过并解封', 'aksrt') . '</button>';
|
||||
echo '<button type="submit" name="decision" value="reject" class="button">' . esc_html__('驳回', 'aksrt') . '</button>';
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
if ($shown < 1) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无申诉', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
public function handle_process_appeal(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
|
||||
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
check_admin_referer('aksrt_users_process_appeal_' . $user_id);
|
||||
|
||||
if ($user_id <= 0 || !in_array($decision, ['approve', 'reject'], true) || !class_exists('\AKSRT\User\BanAppeal')) {
|
||||
$this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'appeals']);
|
||||
}
|
||||
|
||||
$status = $decision === 'approve' ? 'approved' : 'rejected';
|
||||
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, $status);
|
||||
|
||||
$payload = \AKSRT\User\BanAppeal::get_payload($user_id);
|
||||
$payload['admin_note'] = $note;
|
||||
$payload['handled_at'] = current_time('Y-m-d H:i');
|
||||
$payload['handled_by'] = get_current_user_id();
|
||||
$payload['decision'] = $status;
|
||||
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $payload);
|
||||
|
||||
$history = get_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, true);
|
||||
$history = is_array($history) ? $history : [];
|
||||
$history[] = $payload;
|
||||
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, array_slice($history, -30));
|
||||
|
||||
if ($decision === 'approve' && class_exists('\AKSRT\User\Restriction')) {
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('user_restriction') : '';
|
||||
if ($table !== '') {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table} SET status = 'lifted', expires_at = %s WHERE user_id = %d AND type IN ('ban','suspend','mute')",
|
||||
current_time('mysql'),
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
$svc = \AKSRT\User\Restriction::get_instance();
|
||||
if (method_exists($svc, 'clear_user_bans')) {
|
||||
$svc->clear_user_bans($user_id);
|
||||
} elseif (method_exists($svc, 'lift_ban')) {
|
||||
$svc->lift_ban($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Messages\Message')) {
|
||||
$msg = $decision === 'approve'
|
||||
? __('你的禁封申诉已通过,限制已解除。', 'aksrt')
|
||||
: __('你的禁封申诉未通过,可修改后重新提交。', 'aksrt');
|
||||
if ($note !== '') {
|
||||
$msg .= ' ' . sprintf(__('备注:%s', 'aksrt'), $note);
|
||||
}
|
||||
\AKSRT\Messages\Message::get_instance()->send(
|
||||
$user_id,
|
||||
'ban_appeal',
|
||||
$msg,
|
||||
[
|
||||
'title' => $decision === 'approve' ? __('申诉通过', 'aksrt') : __('申诉驳回', 'aksrt'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
do_action('aksrt_ban_appeal_processed', $user_id, $decision, $note);
|
||||
|
||||
$this->redirect(
|
||||
'aksrt_user_ban',
|
||||
$decision === 'approve' ? 'appeal_approved' : 'appeal_rejected',
|
||||
['tab' => 'appeals']
|
||||
);
|
||||
}
|
||||
|
||||
private function pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* Native WordPress identity certification operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusVerify {
|
||||
|
||||
private function count_verify_status(string $status): int {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE status = %s", $status));
|
||||
}
|
||||
|
||||
public function render_verify_page(): void {
|
||||
$this->ensure_cap();
|
||||
$filters = $this->get_verify_filters();
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$result = $this->query_verify_requests($filters, $page_num, 20);
|
||||
$counts = $this->get_verify_status_counts();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
|
||||
$this->flash();
|
||||
echo '<p>' . esc_html(sprintf(__('人工待审 %1$d · 已通过 %2$d · 已驳回 %3$d · 已取消 %4$d', 'aksrt'), $counts['pending'], $counts['verified'], $counts['rejected'], $counts['cancelled'])) . '</p>';
|
||||
$this->render_verify_filters($filters);
|
||||
$this->render_verify_table($result['items']);
|
||||
$this->pagination($result['pages'], $page_num);
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/** @return array{status:string,keyword:string,date_from:string,date_to:string} */
|
||||
private function get_verify_filters(): array {
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
|
||||
if (!in_array($status, ['pending', 'verified', 'rejected', 'cancelled', ''], true)) {
|
||||
$status = 'pending';
|
||||
}
|
||||
return [
|
||||
'status' => $status,
|
||||
'keyword' => isset($_GET['keyword']) ? sanitize_text_field((string) wp_unslash($_GET['keyword'])) : '',
|
||||
'date_from' => $this->sanitize_verify_date($_GET['date_from'] ?? ''),
|
||||
'date_to' => $this->sanitize_verify_date($_GET['date_to'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitize_verify_date($value): string {
|
||||
$value = sanitize_text_field((string) wp_unslash($value));
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
|
||||
}
|
||||
|
||||
/** @return array{pending:int,verified:int,rejected:int,cancelled:int,total:int} */
|
||||
private function get_verify_status_counts(): array {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
$counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'cancelled' => 0, 'total' => 0];
|
||||
$rows = $wpdb->get_results("SELECT status, COUNT(*) total FROM {$table} GROUP BY status", ARRAY_A);
|
||||
foreach ((array) $rows as $row) {
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$total = (int) ($row['total'] ?? 0);
|
||||
$public = \AKSRT\Identity\TransitionService::public_status($status);
|
||||
if (in_array($status, ['manual_pending', 'provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||||
$counts['pending'] += $total;
|
||||
} elseif (isset($counts[$public])) {
|
||||
$counts[$public] += $total;
|
||||
}
|
||||
$counts['total'] += $total;
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/** @return array{items:array<int,array<string,mixed>>,pages:int,total:int} */
|
||||
private function query_verify_requests(array $filters, int $page_num, int $per_page): array {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if ($filters['status'] === 'pending') {
|
||||
$where[] = "a.status IN ('manual_pending','provider_failed','provider_unsupported','provider_expired')";
|
||||
} elseif ($filters['status'] !== '') {
|
||||
$where[] = 'a.status = %s';
|
||||
$params[] = $filters['status'];
|
||||
}
|
||||
if ($filters['keyword'] !== '') {
|
||||
$like = '%' . $wpdb->esc_like($filters['keyword']) . '%';
|
||||
$where[] = '(CAST(u.ID AS CHAR) LIKE %s OR u.display_name LIKE %s OR u.user_login LIKE %s OR u.user_email LIKE %s OR a.public_name LIKE %s OR a.application_uuid LIKE %s)';
|
||||
array_push($params, $like, $like, $like, $like, $like, $like);
|
||||
}
|
||||
if ($filters['date_from'] !== '') {
|
||||
$where[] = 'a.created_at >= %s';
|
||||
$params[] = $filters['date_from'] . ' 00:00:00';
|
||||
}
|
||||
if ($filters['date_to'] !== '') {
|
||||
$where[] = 'a.created_at <= %s';
|
||||
$params[] = $filters['date_to'] . ' 23:59:59';
|
||||
}
|
||||
$where_sql = implode(' AND ', $where);
|
||||
$total_sql = "SELECT COUNT(*) FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE {$where_sql}";
|
||||
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($total_sql, $params)) : (int) $wpdb->get_var($total_sql);
|
||||
$query_params = array_merge($params, [$per_page, ($page_num - 1) * $per_page]);
|
||||
$sql = "SELECT a.*, u.display_name, u.user_login, u.user_email FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE {$where_sql} ORDER BY a.created_at DESC, a.id DESC LIMIT %d OFFSET %d";
|
||||
$items = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
|
||||
return ['items' => is_array($items) ? $items : [], 'pages' => max(1, (int) ceil(max(1, $total) / $per_page)), 'total' => $total];
|
||||
}
|
||||
|
||||
private function render_verify_filters(array $filters): void {
|
||||
echo '<form method="get" style="margin:12px 0;display:flex;gap:8px;align-items:end;flex-wrap:wrap;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
|
||||
echo '<label>' . esc_html__('状态', 'aksrt') . '<br><select name="status">';
|
||||
foreach (['pending' => __('人工待审', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['status'], $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label>';
|
||||
echo '<label>' . esc_html__('搜索', 'aksrt') . '<br><input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" /></label>';
|
||||
echo '<label>' . esc_html__('申请起始日期', 'aksrt') . '<br><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>';
|
||||
echo '<label>' . esc_html__('申请结束日期', 'aksrt') . '<br><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_verify_table(array $items): void {
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>用户</th><th>状态</th><th>申请资料</th><th>审核记录</th><th>操作</th></tr></thead><tbody>';
|
||||
if ($items === []) {
|
||||
echo '<tr><td colspan="5">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$this->render_verify_row($row);
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function render_verify_row(array $row): void {
|
||||
$user_id = (int) $row['user_id'];
|
||||
$status = (string) $row['status'];
|
||||
$application = (new \AKSRT\Identity\ApplicationRepository())->find_by_id((int) $row['id']) ?: $row;
|
||||
$history = (new \AKSRT\Identity\EventRepository())->for_application((int) $row['id']);
|
||||
echo '<tr><td><strong><a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html((string) $row['display_name']) . '</a></strong>';
|
||||
echo '<div class="description">@' . esc_html((string) $row['user_login']) . ' · UID ' . $user_id . '</div><div>' . esc_html((string) $row['user_email']) . '</div></td>';
|
||||
echo '<td>' . wp_kses_post($this->verify_status_badge($status)) . '<div class="description">' . esc_html((string) $row['provider']) . ' / ' . esc_html((string) $row['channel']) . '</div></td>';
|
||||
echo '<td><strong>' . esc_html((string) $row['public_name']) . '</strong><div>' . esc_html((string) $row['type']) . '</div>';
|
||||
echo '<div class="description">UUID: ' . esc_html((string) $row['application_uuid']) . '</div>';
|
||||
if (!empty($row['provider_result_code'])) echo '<div>' . esc_html__('云结果:', 'aksrt') . esc_html((string) $row['provider_result_code']) . '</div>';
|
||||
foreach ((new \AKSRT\Identity\EvidenceService())->items($application, true) as $item) {
|
||||
echo '<a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url($item['thumbnail_url']) . '" alt="" style="width:64px;height:64px;object-fit:cover;margin:8px 8px 0 0;border-radius:4px;" /></a>';
|
||||
}
|
||||
echo '</td><td>';
|
||||
if (!empty($row['review_note'])) echo '<div>' . esc_html((string) $row['review_note']) . '</div>';
|
||||
if ($history !== []) echo '<details><summary>' . esc_html(sprintf(__('事件记录(%d)', 'aksrt'), count($history))) . '</summary><ol>';
|
||||
foreach ($history as $event) echo '<li>' . esc_html((string) $event['created_at'] . ' · ' . (string) $event['event_type'] . ' · ' . (string) $event['to_status']) . '</li>';
|
||||
if ($history !== []) echo '</ol></details>';
|
||||
echo '</td><td>';
|
||||
$this->render_verify_actions($row);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
|
||||
private function verify_status_badge(string $status): string {
|
||||
$labels = ['manual_pending' => __('人工待审', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), 'provider_pending' => __('等待核身', 'aksrt'), 'provider_processing' => __('云端处理中', 'aksrt'), 'provider_failed' => __('云认证失败', 'aksrt'), 'provider_unsupported' => __('云端不支持', 'aksrt'), 'provider_expired' => __('会话已过期', 'aksrt')];
|
||||
return '<span style="display:inline-block;padding:2px 8px;border:1px solid currentColor;border-radius:999px;">' . esc_html($labels[$status] ?? $status) . '</span>';
|
||||
}
|
||||
|
||||
private function render_verify_actions(array $row): void {
|
||||
$status = (string) $row['status'];
|
||||
if (in_array($status, ['manual_pending', 'provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||||
foreach (['verified' => __('通过认证', 'aksrt'), 'rejected' => __('驳回申请', 'aksrt')] as $next => $label) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:6px;margin-bottom:8px;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" /><input type="hidden" name="user_id" value="' . (int) $row['user_id'] . '" /><input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '" /><input type="hidden" name="expected_status" value="' . esc_attr($status) . '" /><input type="hidden" name="status" value="' . esc_attr($next) . '" />';
|
||||
wp_nonce_field('aksrt_users_review_verify_' . (int) $row['user_id']);
|
||||
echo '<textarea name="note" rows="2" ' . ($next === 'rejected' ? 'required' : '') . '></textarea><button class="button ' . ($next === 'verified' ? 'button-primary' : '') . '">' . esc_html($label) . '</button></form>';
|
||||
}
|
||||
} elseif ($status === 'verified') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '"><input type="hidden" name="action" value="aksrt_users_review_verify" /><input type="hidden" name="user_id" value="' . (int) $row['user_id'] . '" /><input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '" /><input type="hidden" name="expected_status" value="verified" /><input type="hidden" name="status" value="cancelled" />';
|
||||
wp_nonce_field('aksrt_users_review_verify_' . (int) $row['user_id']);
|
||||
echo '<textarea name="note" required></textarea><button class="button button-link-delete">' . esc_html__('取消认证', 'aksrt') . '</button></form>';
|
||||
} else {
|
||||
echo '<span class="description">' . esc_html__('无需人工操作', 'aksrt') . '</span>';
|
||||
}
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-top:8px;" onsubmit="return confirm(\'' . esc_js(__('确定删除这条身份认证记录?关联事件和人工材料也会删除。', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_delete_verify" /><input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '" />';
|
||||
wp_nonce_field('aksrt_users_delete_verify_' . (string) $row['application_uuid']);
|
||||
echo '<button class="button button-link-delete">' . esc_html__('删除记录', 'aksrt') . '</button></form>';
|
||||
}
|
||||
|
||||
public function handle_review_verify(): void {
|
||||
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$user_id = (int) ($_POST['user_id'] ?? 0);
|
||||
check_admin_referer('aksrt_users_review_verify_' . $user_id);
|
||||
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
|
||||
$expected = sanitize_key((string) wp_unslash($_POST['expected_status'] ?? ''));
|
||||
$status = sanitize_key((string) wp_unslash($_POST['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? ''));
|
||||
$application = (new \AKSRT\Identity\ApplicationRepository())->find_by_uuid($uuid);
|
||||
$result = (!$application || (int) $application['user_id'] !== $user_id || (string) $application['status'] !== $expected)
|
||||
? new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'))
|
||||
: \AKSRT\User\AuthApply::review($user_id, $status, $note, get_current_user_id());
|
||||
$extra = ['status' => $status === 'cancelled' ? 'cancelled' : 'pending'];
|
||||
if (is_wp_error($result)) $extra['aksrt_verify_error'] = $result->get_error_message();
|
||||
$this->redirect('aksrt_user_auth', is_wp_error($result) ? 'verify_failed' : 'verify_updated', $extra);
|
||||
}
|
||||
|
||||
public function handle_delete_verify(): void {
|
||||
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
|
||||
check_admin_referer('aksrt_users_delete_verify_' . $uuid);
|
||||
$repo = new \AKSRT\Identity\ApplicationRepository();
|
||||
$application = $repo->find_by_uuid($uuid);
|
||||
if (!$application) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]);
|
||||
}
|
||||
(new \AKSRT\Identity\EvidenceService())->delete((array) ($application['evidence_ids'] ?? []));
|
||||
(new \AKSRT\Identity\EventRepository())->delete_for_application((int) $application['id']);
|
||||
$deleted = $repo->delete((int) $application['id']);
|
||||
$this->redirect('aksrt_user_auth', $deleted ? 'verify_deleted' : 'verify_failed', $deleted ? [] : ['aksrt_verify_error' => __('认证记录删除失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
private function get_verify_history(int $user_id): array {
|
||||
return \AKSRT\User\AuthApply::get_review_history($user_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
/**
|
||||
* Native WP nav-menu item options (aksrt_menu_options) with senior submenu JSON.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class NavMenuOptions {
|
||||
|
||||
public static function init(): void {
|
||||
add_action('wp_nav_menu_item_custom_fields', [self::class, 'render_fields'], 10, 2);
|
||||
add_action('wp_update_nav_menu_item', [self::class, 'save'], 10, 2);
|
||||
add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin']);
|
||||
}
|
||||
|
||||
public static function enqueue_admin(string $hook): void {
|
||||
if ($hook !== 'nav-menus.php') {
|
||||
return;
|
||||
}
|
||||
$path = get_template_directory() . '/assets/js/admin-nav-menu-senior.js';
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script(
|
||||
'aksrt-admin-nav-menu-senior',
|
||||
get_template_directory_uri() . '/assets/js/admin-nav-menu-senior.js',
|
||||
['jquery'],
|
||||
(string) filemtime($path),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $item_id
|
||||
* @param \WP_Post $item
|
||||
*/
|
||||
public static function render_fields($item_id, $item): void {
|
||||
$item_id = (int) $item_id;
|
||||
$opts = get_post_meta($item_id, 'aksrt_menu_options', true);
|
||||
$opts = is_array($opts) ? $opts : [];
|
||||
$icon = (string) ($opts['icon'] ?? '');
|
||||
$badge = (string) ($opts['badge'] ?? '');
|
||||
$badge_class = (string) ($opts['badge_class'] ?? '');
|
||||
$submenu_s = !empty($opts['submenu_s']);
|
||||
$submenu_type = (string) ($opts['submenu_type'] ?? '');
|
||||
$graphic_json = !empty($opts['graphic_card_opts'])
|
||||
? wp_json_encode($opts['graphic_card_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
: self::sample_graphic_json();
|
||||
$multi_json = !empty($opts['multi_column_links_opts'])
|
||||
? wp_json_encode($opts['multi_column_links_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
: self::sample_multi_json();
|
||||
?>
|
||||
<p class="field-aksrt-icon description description-wide">
|
||||
<label for="edit-menu-item-aksrt-icon-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('图标 class(如 fas fa-home)', 'aksrt'); ?><br>
|
||||
<input type="text" id="edit-menu-item-aksrt-icon-<?php echo esc_attr((string) $item_id); ?>"
|
||||
class="widefat" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][icon]"
|
||||
value="<?php echo esc_attr($icon); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-badge description description-thin">
|
||||
<label for="edit-menu-item-aksrt-badge-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('徽章文字', 'aksrt'); ?><br>
|
||||
<input type="text" id="edit-menu-item-aksrt-badge-<?php echo esc_attr((string) $item_id); ?>"
|
||||
class="widefat" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][badge]"
|
||||
value="<?php echo esc_attr($badge); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-badge-class description description-thin">
|
||||
<label for="edit-menu-item-aksrt-badge-class-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('徽章 class', 'aksrt'); ?><br>
|
||||
<input type="text" id="edit-menu-item-aksrt-badge-class-<?php echo esc_attr((string) $item_id); ?>"
|
||||
class="widefat" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][badge_class]"
|
||||
value="<?php echo esc_attr($badge_class); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-submenu description description-wide">
|
||||
<label>
|
||||
<input type="checkbox" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][submenu_s]" value="1" <?php checked($submenu_s); ?>>
|
||||
<?php esc_html_e('启用高级子菜单(仅一级菜单)', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-submenu-type description description-wide">
|
||||
<label for="edit-menu-item-aksrt-submenu-type-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('高级子菜单类型', 'aksrt'); ?><br>
|
||||
<select id="edit-menu-item-aksrt-submenu-type-<?php echo esc_attr((string) $item_id); ?>"
|
||||
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][submenu_type]">
|
||||
<option value="" <?php selected($submenu_type, ''); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
|
||||
<option value="graphic_card" <?php selected($submenu_type, 'graphic_card'); ?>><?php esc_html_e('图文卡片', 'aksrt'); ?></option>
|
||||
<option value="multi_column_links" <?php selected($submenu_type, 'multi_column_links'); ?>><?php esc_html_e('多列链接', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-graphic description description-wide">
|
||||
<label for="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('图文卡片 JSON(type=graphic_card)', 'aksrt'); ?><br>
|
||||
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>"
|
||||
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][graphic_card_opts_json]"><?php echo esc_textarea((string) $graphic_json); ?></textarea>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-multi description description-wide">
|
||||
<label for="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>">
|
||||
<?php esc_html_e('多列链接 JSON(type=multi_column_links)', 'aksrt'); ?><br>
|
||||
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>"
|
||||
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][multi_column_links_opts_json]"><?php echo esc_textarea((string) $multi_json); ?></textarea>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $menu_id
|
||||
* @param int $menu_item_db_id
|
||||
*/
|
||||
public static function save($menu_id, $menu_item_db_id): void {
|
||||
$menu_item_db_id = (int) $menu_item_db_id;
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
if (empty($_POST['aksrt_menu_options'][ $menu_item_db_id ]) || !is_array($_POST['aksrt_menu_options'][ $menu_item_db_id ])) {
|
||||
return;
|
||||
}
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$raw = wp_unslash($_POST['aksrt_menu_options'][ $menu_item_db_id ]);
|
||||
|
||||
$clean = [
|
||||
'icon' => sanitize_text_field((string) ($raw['icon'] ?? '')),
|
||||
'badge' => sanitize_text_field((string) ($raw['badge'] ?? '')),
|
||||
'badge_class' => sanitize_html_class((string) ($raw['badge_class'] ?? '')),
|
||||
'submenu_s' => !empty($raw['submenu_s']),
|
||||
'submenu_type' => sanitize_key((string) ($raw['submenu_type'] ?? '')),
|
||||
];
|
||||
|
||||
$graphic = self::decode_json_field((string) ($raw['graphic_card_opts_json'] ?? ''));
|
||||
if (is_array($graphic)) {
|
||||
$clean['graphic_card_opts'] = self::sanitize_graphic_opts($graphic);
|
||||
}
|
||||
$multi = self::decode_json_field((string) ($raw['multi_column_links_opts_json'] ?? ''));
|
||||
if (is_array($multi)) {
|
||||
$clean['multi_column_links_opts'] = self::sanitize_multi_opts($multi);
|
||||
}
|
||||
|
||||
update_post_meta($menu_item_db_id, 'aksrt_menu_options', $clean);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private static function decode_json_field(string $json): ?array {
|
||||
$json = trim($json);
|
||||
if ($json === '') {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $opts
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function sanitize_graphic_opts(array $opts): array {
|
||||
$items_in = is_array($opts['items'] ?? null) ? $opts['items'] : [];
|
||||
$items = [];
|
||||
foreach ($items_in as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'img' => esc_url_raw((string) ($item['img'] ?? '')),
|
||||
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
||||
'badge' => sanitize_text_field((string) ($item['badge'] ?? '')),
|
||||
'badge_class' => sanitize_html_class((string) ($item['badge_class'] ?? '')),
|
||||
'link' => [
|
||||
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
|
||||
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
|
||||
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
|
||||
],
|
||||
];
|
||||
}
|
||||
return [
|
||||
'arrangement' => sanitize_key((string) ($opts['arrangement'] ?? '')),
|
||||
'align' => sanitize_key((string) ($opts['align'] ?? '')),
|
||||
'size' => sanitize_key((string) ($opts['size'] ?? '')),
|
||||
'img_scale' => max(0, min(200, (int) ($opts['img_scale'] ?? 100))),
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $opts
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function sanitize_multi_opts(array $opts): array {
|
||||
$columns_in = is_array($opts['columns'] ?? null) ? $opts['columns'] : [];
|
||||
$columns = [];
|
||||
foreach ($columns_in as $col) {
|
||||
if (!is_array($col)) {
|
||||
continue;
|
||||
}
|
||||
$items = [];
|
||||
if (!empty($col['items']) && is_array($col['items'])) {
|
||||
foreach ($col['items'] as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
||||
'icon' => sanitize_text_field((string) ($item['icon'] ?? '')),
|
||||
'img_icon' => esc_url_raw((string) ($item['img_icon'] ?? '')),
|
||||
'link' => [
|
||||
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
|
||||
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
|
||||
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
$columns[] = [
|
||||
'title' => sanitize_text_field((string) ($col['title'] ?? '')),
|
||||
'icon' => sanitize_text_field((string) ($col['icon'] ?? '')),
|
||||
'img_icon' => esc_url_raw((string) ($col['img_icon'] ?? '')),
|
||||
'link' => [
|
||||
'url' => esc_url_raw((string) ($col['link']['url'] ?? '')),
|
||||
'text' => sanitize_text_field((string) ($col['link']['text'] ?? '')),
|
||||
'target' => sanitize_text_field((string) ($col['link']['target'] ?? '')),
|
||||
],
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
return ['columns' => $columns];
|
||||
}
|
||||
|
||||
private static function sample_graphic_json(): string {
|
||||
$sample = [
|
||||
'arrangement' => '',
|
||||
'size' => 'md',
|
||||
'img_scale' => 100,
|
||||
'items' => [
|
||||
[
|
||||
'img' => '',
|
||||
'title' => '卡片标题',
|
||||
'badge' => '',
|
||||
'badge_class' => 'jb-red',
|
||||
'link' => ['url' => home_url('/'), 'text' => '', 'target' => ''],
|
||||
],
|
||||
],
|
||||
];
|
||||
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
private static function sample_multi_json(): string {
|
||||
$sample = [
|
||||
'columns' => [
|
||||
[
|
||||
'title' => '栏目一',
|
||||
'icon' => 'fas fa-folder',
|
||||
'link' => ['url' => '', 'text' => '', 'target' => ''],
|
||||
'items' => [
|
||||
['title' => '链接 A', 'icon' => '', 'link' => ['url' => home_url('/'), 'text' => '', 'target' => '']],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PageDisplayController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
private $admin;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function set_admin(Admin $admin): void {
|
||||
$this->admin = $admin;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Banners (generic CRUD — hero, banner kinds)
|
||||
register_rest_route(self::API_NAMESPACE, '/banners', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_banners'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/banners', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_create_banner'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/banners/(?P<id>[\w-]+)', [
|
||||
'methods' => ['PUT', 'DELETE'],
|
||||
'callback' => [$this, 'rest_handle_banner_item'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalize_banner_kind($kind): string {
|
||||
return in_array($kind, ['hero', 'banner'], true) ? $kind : 'hero';
|
||||
}
|
||||
|
||||
private function get_banner_kind_query(string $kind): array {
|
||||
if ('banner' === $kind) {
|
||||
return [
|
||||
[
|
||||
'key' => '_aksrt_kind',
|
||||
'value' => 'banner',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => '_aksrt_kind',
|
||||
'value' => 'hero',
|
||||
],
|
||||
[
|
||||
'key' => '_aksrt_kind',
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function rest_get_banners(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$kind = $this->normalize_banner_kind($request->get_param('kind') ?: 'hero');
|
||||
$args = [
|
||||
'post_type' => 'aksrt_banner',
|
||||
'posts_per_page' => 100,
|
||||
'post_status' => 'any',
|
||||
'orderby' => 'menu_order',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => $this->get_banner_kind_query($kind),
|
||||
];
|
||||
$posts = get_posts($args);
|
||||
$banners = array_map(function ($post) {
|
||||
$layers_json = get_post_meta($post->ID, '_layers', true);
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'subtitle' => get_post_meta($post->ID, '_subtitle', true),
|
||||
'description' => get_post_meta($post->ID, '_description', true),
|
||||
'image_url' => get_post_meta($post->ID, '_image_url', true),
|
||||
'video_url' => get_post_meta($post->ID, '_video_url', true),
|
||||
'link_url' => get_post_meta($post->ID, '_link_url', true),
|
||||
'link_target' => get_post_meta($post->ID, '_link_target', true) ?: '_self',
|
||||
'text_align' => get_post_meta($post->ID, '_text_align', true) ?: 'left',
|
||||
'device_visibility' => get_post_meta($post->ID, '_device_visibility', true) ?: 'all',
|
||||
// Match frontend aksrt_get_banners(): missing _enabled means enabled.
|
||||
'enabled' => get_post_meta($post->ID, '_enabled', true) !== '0',
|
||||
'show_text' => get_post_meta($post->ID, '_show_text', true) !== '0',
|
||||
'show_meta' => get_post_meta($post->ID, '_show_meta', true) === '1',
|
||||
'kind' => get_post_meta($post->ID, '_aksrt_kind', true) ?: 'hero',
|
||||
'sort_order' => $post->menu_order,
|
||||
'text_size_pc' => get_post_meta($post->ID, '_text_size_pc', true) ?: '',
|
||||
'text_size_m' => get_post_meta($post->ID, '_text_size_m', true) ?: '',
|
||||
'text_parallax' => get_post_meta($post->ID, '_text_parallax', true) === '1',
|
||||
'layers' => is_string($layers_json) && $layers_json !== '' ? (json_decode($layers_json, true) ?: []) : [],
|
||||
];
|
||||
}, $posts);
|
||||
return rest_ensure_response(['code' => 'success', 'data' => $banners]);
|
||||
}
|
||||
|
||||
public function rest_create_banner(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$kind = $this->normalize_banner_kind($params['kind'] ?? 'hero');
|
||||
$title = sanitize_text_field($params['title'] ?? '');
|
||||
$post_id = wp_insert_post([
|
||||
// Title is optional in admin UI; keep a non-empty WP title for list readability.
|
||||
'post_title' => '' !== $title ? $title : ('banner' === $kind ? 'Banner Slide' : 'Hero Slide'),
|
||||
'post_type' => 'aksrt_banner',
|
||||
'post_status' => 'publish',
|
||||
'menu_order' => (int) ($params['sort_order'] ?? 0),
|
||||
]);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return new \WP_REST_Response(['code' => 'error', 'message' => $post_id->get_error_message()], 400);
|
||||
}
|
||||
|
||||
update_post_meta($post_id, '_image_url', esc_url_raw($params['image_url'] ?? ''));
|
||||
update_post_meta($post_id, '_link_url', esc_url_raw($params['link_url'] ?? ''));
|
||||
update_post_meta($post_id, '_link_target', in_array($params['link_target'] ?? '', ['_self', '_blank'], true) ? $params['link_target'] : '_self');
|
||||
update_post_meta($post_id, '_subtitle', sanitize_text_field($params['subtitle'] ?? ''));
|
||||
update_post_meta($post_id, '_description', sanitize_textarea_field($params['description'] ?? ''));
|
||||
update_post_meta($post_id, '_video_url', esc_url_raw($params['video_url'] ?? ''));
|
||||
update_post_meta($post_id, '_text_align', in_array($params['text_align'] ?? '', ['left', 'center', 'right'], true) ? $params['text_align'] : 'left');
|
||||
update_post_meta($post_id, '_device_visibility', in_array($params['device_visibility'] ?? '', ['all', 'pc', 'mobile'], true) ? $params['device_visibility'] : 'all');
|
||||
update_post_meta($post_id, '_enabled', !array_key_exists('enabled', $params) || !empty($params['enabled']) ? '1' : '0');
|
||||
update_post_meta($post_id, '_show_text', !array_key_exists('show_text', $params) || !empty($params['show_text']) ? '1' : '0');
|
||||
update_post_meta($post_id, '_show_meta', !empty($params['show_meta']) ? '1' : '0');
|
||||
update_post_meta($post_id, '_aksrt_kind', $kind);
|
||||
update_post_meta($post_id, '_text_size_pc', sanitize_text_field($params['text_size_pc'] ?? ''));
|
||||
update_post_meta($post_id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));
|
||||
update_post_meta($post_id, '_text_parallax', !empty($params['text_parallax']) ? '1' : '0');
|
||||
if (isset($params['layers']) && is_array($params['layers'])) {
|
||||
update_post_meta($post_id, '_layers', wp_json_encode(array_values($params['layers'])));
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'data' => ['id' => $post_id], 'message' => __('幻灯片已创建', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_banner_item(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$id = absint($request['id']);
|
||||
if (!$id || get_post_type($id) !== 'aksrt_banner') {
|
||||
return new \WP_REST_Response(['code' => 'not_found', 'message' => __('幻灯片不存在', 'aksrt')], 404);
|
||||
}
|
||||
|
||||
if ($request->get_method() === 'DELETE') {
|
||||
wp_delete_post($id, true);
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('幻灯片已删除', 'aksrt')]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$kind = $this->normalize_banner_kind($params['kind'] ?? get_post_meta($id, '_aksrt_kind', true) ?: 'hero');
|
||||
// Optional title: empty is allowed (UI shows “未命名”); do not 422.
|
||||
$title = sanitize_text_field($params['title'] ?? '');
|
||||
|
||||
wp_update_post([
|
||||
'ID' => $id,
|
||||
'post_title' => $title,
|
||||
]);
|
||||
|
||||
update_post_meta($id, '_aksrt_kind', $kind);
|
||||
update_post_meta($id, '_subtitle', sanitize_text_field($params['subtitle'] ?? ''));
|
||||
update_post_meta($id, '_description', sanitize_textarea_field($params['description'] ?? ''));
|
||||
update_post_meta($id, '_image_url', esc_url_raw($params['image_url'] ?? ''));
|
||||
update_post_meta($id, '_video_url', esc_url_raw($params['video_url'] ?? ''));
|
||||
update_post_meta($id, '_link_url', esc_url_raw($params['link_url'] ?? ''));
|
||||
update_post_meta($id, '_link_target', in_array($params['link_target'] ?? '', ['_blank', '_self'], true) ? $params['link_target'] : '_self');
|
||||
update_post_meta($id, '_text_align', in_array($params['text_align'] ?? '', ['left', 'center', 'right'], true) ? $params['text_align'] : 'left');
|
||||
update_post_meta($id, '_device_visibility', in_array($params['device_visibility'] ?? '', ['all', 'pc', 'mobile'], true) ? $params['device_visibility'] : 'all');
|
||||
update_post_meta($id, '_enabled', !array_key_exists('enabled', $params) || !empty($params['enabled']) ? '1' : '0');
|
||||
update_post_meta($id, '_show_text', !array_key_exists('show_text', $params) || !empty($params['show_text']) ? '1' : '0');
|
||||
update_post_meta($id, '_show_meta', !empty($params['show_meta']) ? '1' : '0');
|
||||
update_post_meta($id, '_text_size_pc', sanitize_text_field($params['text_size_pc'] ?? ''));
|
||||
update_post_meta($id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));
|
||||
update_post_meta($id, '_text_parallax', !empty($params['text_parallax']) ? '1' : '0');
|
||||
|
||||
if (isset($params['layers']) && is_array($params['layers'])) {
|
||||
update_post_meta($id, '_layers', wp_json_encode(array_values($params['layers'])));
|
||||
} else {
|
||||
delete_post_meta($id, '_layers');
|
||||
}
|
||||
|
||||
if (isset($params['sort_order'])) {
|
||||
wp_update_post([
|
||||
'ID' => $id,
|
||||
'menu_order' => (int) $params['sort_order'],
|
||||
]);
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('幻灯片已更新', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,797 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PaymentController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
$this->register_section_routes('payment', 'payment');
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_payment_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Revenue stats (with optional time-series)
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/revenue', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_revenue'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'date_from' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'date_to' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'granularity' => ['default' => 'day', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'time_series' => ['default' => false, 'sanitize_callback' => 'rest_sanitize_boolean'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Withdrawal list — unified pagination: page + per_page
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_withdrawals'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Withdrawal status update
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/withdrawals/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_withdrawal_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Charge Card ──
|
||||
|
||||
// Charge card list
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_card_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Charge card create
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Charge card batch create
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/batch', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'batch_create_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Charge card disable
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P<id>\d+)/disable', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'disable_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Rebate ──
|
||||
|
||||
// Rebate list
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/rebate/log', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_rebate_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_rebate_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Rebate settle
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P<id>\d+)/settle', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'settle_rebate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Rebate cancel
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P<id>\d+)/cancel', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'cancel_rebate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_payment_overview(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return $this->error('设置模块未初始化', 500);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
|
||||
$recharge = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
$vip = \AKSRT\Settings\Settings::get_payment_vip_settings();
|
||||
$revenue = \AKSRT\Settings\Settings::get_payment_revenue_settings();
|
||||
$affiliate = \AKSRT\Settings\Settings::get_payment_affiliate_settings();
|
||||
$gateway = \AKSRT\Settings\Settings::get_payment_gateway_settings();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'paid' => $paid,
|
||||
'recharge' => $recharge,
|
||||
'vip' => $vip,
|
||||
'revenue' => $revenue,
|
||||
'affiliate' => $affiliate,
|
||||
'gateway' => $gateway,
|
||||
'currency_name' => \AKSRT\Settings\Settings::get_currency_name(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /payment/revenue — revenue stats with optional time-series
|
||||
*/
|
||||
public function get_revenue(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0];
|
||||
if (class_exists('\AKSRT\Payment\Revenue')) {
|
||||
$revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$withdrawal_stats = ['total' => 0, 'pending' => 0, 'completed' => 0, 'total_paid' => 0];
|
||||
if (class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$withdrawal_stats = \AKSRT\Payment\Withdrawal::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$response = [
|
||||
'revenue' => $revenue_stats,
|
||||
'withdrawal' => $withdrawal_stats,
|
||||
];
|
||||
|
||||
// Time-series data
|
||||
if ($request->get_param('time_series')) {
|
||||
$response['time_series'] = $this->get_revenue_time_series($request);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $response]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build revenue time-series data.
|
||||
*/
|
||||
private function get_revenue_time_series(\WP_REST_Request $request): array {
|
||||
global $wpdb;
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
$granularity = sanitize_text_field((string) $request->get_param('granularity'));
|
||||
|
||||
// Default: last 30 days
|
||||
if (!$date_from) {
|
||||
$date_from = gmdate('Y-m-d', strtotime('-30 days'));
|
||||
}
|
||||
if (!$date_to) {
|
||||
$date_to = gmdate('Y-m-d');
|
||||
}
|
||||
|
||||
// Granularity: day, week, month
|
||||
$date_format = '%Y-%m-%d';
|
||||
if ($granularity === 'week') {
|
||||
$date_format = '%x-W%v';
|
||||
} elseif ($granularity === 'month') {
|
||||
$date_format = '%Y-%m';
|
||||
}
|
||||
|
||||
$table = $prefix . 'aksrt_revenue';
|
||||
$results = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT DATE_FORMAT(created_at, %s) AS period,
|
||||
SUM(amount) AS total_amount,
|
||||
COUNT(*) AS order_count
|
||||
FROM {$table}
|
||||
WHERE created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
|
||||
GROUP BY period
|
||||
ORDER BY period ASC",
|
||||
$date_format,
|
||||
$date_from,
|
||||
$date_to
|
||||
));
|
||||
|
||||
$series = [];
|
||||
if ($results) {
|
||||
foreach ($results as $row) {
|
||||
$series[] = [
|
||||
'date' => (string) $row->period,
|
||||
'amount' => (float) $row->total_amount,
|
||||
'count' => (int) $row->order_count,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /payment/withdrawals — 提现列表,统一 page + per_page 分页
|
||||
*/
|
||||
public function get_withdrawals(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1, 'stats' => []]]);
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
|
||||
|
||||
$items = \AKSRT\Payment\Withdrawal::get_instance()->get_all([
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
]);
|
||||
|
||||
$total = \AKSRT\Payment\Withdrawal::get_instance()->get_count(['status' => $status]);
|
||||
$stats = \AKSRT\Payment\Withdrawal::get_instance()->get_stats();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
'stats' => $stats,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/withdrawals/{id} — 更新提现状态
|
||||
*/
|
||||
public function update_withdrawal_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
return $this->error('提现模块未初始化', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$new_status = sanitize_key((string) ($params['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
|
||||
$valid_statuses = ['pending', 'approved', 'processing', 'completed', 'rejected'];
|
||||
if (!in_array($new_status, $valid_statuses, true)) {
|
||||
return $this->error('无效的提现状态');
|
||||
}
|
||||
|
||||
if ($id <= 0) {
|
||||
return $this->error('无效的提现 ID');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\Withdrawal::get_instance()->update_status($id, $new_status, $note);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('提现状态已更新', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Charge Card
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* GET /payment/charge-cards — 充值卡列表
|
||||
*/
|
||||
public function get_charge_cards(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('charge_card');
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($date_from !== '') {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = $date_from . ' 00:00:00';
|
||||
}
|
||||
if ($date_to !== '') {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = $date_to . ' 23:59:59';
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_charge_card_admin_item($row);
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
|
||||
(string) ($item['code'] ?? ''),
|
||||
(string) ($item['password'] ?? ''),
|
||||
(string) ($item['card'] ?? ''),
|
||||
(string) ($item['type_label'] ?? ''),
|
||||
(string) ($item['note'] ?? ''),
|
||||
(string) ($item['used_by_user']['display_name'] ?? ''),
|
||||
(string) ($item['used_by_user']['email'] ?? ''),
|
||||
])), $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$stats = \AKSRT\Payment\ChargeCard::get_instance()->get_stats();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values(array_slice($items, $offset, $per_page)),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
|
||||
'stats' => $stats,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('全部状态', 'aksrt'), 'value' => ''],
|
||||
['label' => __('未使用', 'aksrt'), 'value' => 'active'],
|
||||
['label' => __('已使用', 'aksrt'), 'value' => 'used'],
|
||||
['label' => __('已过期', 'aksrt'), 'value' => 'expired'],
|
||||
['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards — 创建充值卡
|
||||
*/
|
||||
public function create_charge_card(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? ''));
|
||||
if ($password === '') {
|
||||
return $this->error('卡密不能为空');
|
||||
}
|
||||
|
||||
$id = \AKSRT\Payment\ChargeCard::get_instance()->create(array_merge($params, [
|
||||
'type' => $type,
|
||||
'password' => $password,
|
||||
]));
|
||||
|
||||
if ($id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('充值卡已创建', 'aksrt'),
|
||||
'data' => ['id' => $id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return $this->error(__('创建失败(可能卡密已存在或参数无效)', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards/batch — 批量生成充值卡
|
||||
*/
|
||||
public function batch_create_charge_cards(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
$count = max(1, min(100, (int) ($params['count'] ?? 1)));
|
||||
$length = max(8, min(32, (int) ($params['length'] ?? 16)));
|
||||
$prefix = sanitize_text_field((string) ($params['prefix'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
$expires = sanitize_text_field((string) ($params['expires_at'] ?? ''));
|
||||
|
||||
$meta_fields = [
|
||||
'amount' => (float) ($params['amount'] ?? 0),
|
||||
'level' => (int) ($params['level'] ?? 1),
|
||||
'days' => (int) ($params['days'] ?? $params['vip_days'] ?? 30),
|
||||
'points' => (int) ($params['points'] ?? $params['credits'] ?? 0),
|
||||
];
|
||||
|
||||
if ($type === 'balance' && $meta_fields['amount'] <= 0) {
|
||||
return $this->error('余额卡金额必须大于 0');
|
||||
}
|
||||
if ($type === 'credits' && $meta_fields['points'] <= 0) {
|
||||
return $this->error('积分卡积分必须大于 0');
|
||||
}
|
||||
|
||||
$codes = \AKSRT\Payment\ChargeCard::get_instance()->batch_create(
|
||||
$type,
|
||||
$meta_fields,
|
||||
$count,
|
||||
$length,
|
||||
$prefix,
|
||||
$expires,
|
||||
$note
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)),
|
||||
'data' => ['codes' => $codes, 'count' => count($codes), 'type' => $type],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards/{id}/disable — 禁用充值卡
|
||||
*/
|
||||
public function disable_charge_card(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\ChargeCard::get_instance()->disable($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('充值卡已禁用', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('操作失败', 'aksrt'));
|
||||
}
|
||||
|
||||
public function get_charge_card_detail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$table = \AKSRT\Core\DBMigrator::table('charge_card');
|
||||
|
||||
if ($id <= 0 || !$table) {
|
||||
return $this->error(__('充值卡不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('充值卡不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'card' => $this->build_charge_card_admin_item($row),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Rebate
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* GET /payment/rebate/log — 返佣记录列表
|
||||
*/
|
||||
public function get_rebate_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('rebate_log');
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$type = sanitize_key((string) $request->get_param('type'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$user_id = (int) $request->get_param('user_id');
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($type !== '') {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
if ($date_from !== '') {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = $date_from . ' 00:00:00';
|
||||
}
|
||||
if ($date_to !== '') {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = $date_to . ' 23:59:59';
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_rebate_admin_item($row);
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
|
||||
(string) ($item['order_number'] ?? ''),
|
||||
(string) ($item['note'] ?? ''),
|
||||
(string) ($item['user']['display_name'] ?? ''),
|
||||
(string) ($item['source_user']['display_name'] ?? ''),
|
||||
])), $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$stats = \AKSRT\Payment\Rebate::get_instance()->get_stats();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values(array_slice($items, $offset, $per_page)),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
|
||||
'stats' => $stats,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('全部状态', 'aksrt'), 'value' => ''],
|
||||
['label' => __('待结算', 'aksrt'), 'value' => 'pending'],
|
||||
['label' => __('已结算', 'aksrt'), 'value' => 'settled'],
|
||||
['label' => __('已取消', 'aksrt'), 'value' => 'cancelled'],
|
||||
],
|
||||
'types' => [
|
||||
['label' => __('全部类型', 'aksrt'), 'value' => ''],
|
||||
['label' => __('订单返佣', 'aksrt'), 'value' => 'order'],
|
||||
['label' => __('邀请返佣', 'aksrt'), 'value' => 'invite'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/rebate/{id}/settle — 结算返佣
|
||||
*/
|
||||
public function settle_rebate(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->error('返佣系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\Rebate::get_instance()->settle($id);
|
||||
|
||||
if ($success) {
|
||||
$this->append_rebate_process_log($id, 'settled', $note);
|
||||
return $this->success(['message' => __('返佣已结算', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('结算失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/rebate/{id}/cancel — 取消返佣
|
||||
*/
|
||||
public function cancel_rebate(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->error('返佣系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\Rebate::get_instance()->cancel($id);
|
||||
|
||||
if ($success) {
|
||||
$this->append_rebate_process_log($id, 'cancelled', $note);
|
||||
return $this->success(['message' => __('返佣已取消', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('操作失败', 'aksrt'));
|
||||
}
|
||||
|
||||
public function get_rebate_detail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$table = \AKSRT\Core\DBMigrator::table('rebate_log');
|
||||
|
||||
if ($id <= 0 || !$table) {
|
||||
return $this->error(__('返佣记录不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('返佣记录不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'record' => $this->build_rebate_admin_item($row),
|
||||
'history' => $this->get_rebate_process_log($id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_charge_card_admin_item(array $row): array {
|
||||
$used_by = (int) ($row['used_by'] ?? 0);
|
||||
$user = $used_by > 0 ? get_userdata($used_by) : null;
|
||||
$status = sanitize_key((string) ($row['status'] ?? 'active'));
|
||||
$type = sanitize_key((string) ($row['type'] ?? 'balance'));
|
||||
$statusMap = [
|
||||
'active' => __('未使用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
'expired' => __('已过期', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
];
|
||||
$typeMap = [
|
||||
'balance' => __('余额卡', 'aksrt'),
|
||||
'vip' => __('会员卡', 'aksrt'),
|
||||
'credits' => __('积分卡', 'aksrt'),
|
||||
];
|
||||
|
||||
$meta = [];
|
||||
if (!empty($row['meta'])) {
|
||||
if (is_array($row['meta'])) {
|
||||
$meta = $row['meta'];
|
||||
} else {
|
||||
$decoded = json_decode((string) $row['meta'], true);
|
||||
if (is_array($decoded)) {
|
||||
$meta = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$password = (string) ($row['password'] ?? $row['code'] ?? '');
|
||||
$card_no = (string) ($row['card'] ?? '');
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'code' => $password,
|
||||
'password' => $password,
|
||||
'card' => $card_no,
|
||||
'type' => $type,
|
||||
'type_label' => $typeMap[$type] ?? $type,
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'balance' => (float) ($row['balance'] ?? 0),
|
||||
'meta' => $meta,
|
||||
'level' => (int) ($meta['level'] ?? 0),
|
||||
'days' => (int) ($meta['days'] ?? 0),
|
||||
'points' => (int) ($meta['points'] ?? 0),
|
||||
'order_number' => (string) ($row['order_number'] ?? ''),
|
||||
'status' => $status,
|
||||
'status_label' => $statusMap[$status] ?? $status,
|
||||
'used_by' => $used_by,
|
||||
'used_at' => (string) ($row['used_at'] ?? ''),
|
||||
'expires_at' => (string) ($row['expires_at'] ?? ''),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'note' => (string) ($row['note'] ?? ''),
|
||||
'used_by_user' => $user instanceof \WP_User ? [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function build_rebate_admin_item(array $row): array {
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$source_user_id = (int) ($row['source_user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$source_user = $source_user_id > 0 ? get_userdata($source_user_id) : null;
|
||||
$status = sanitize_key((string) ($row['status'] ?? 'pending'));
|
||||
$type = sanitize_key((string) ($row['type'] ?? 'order'));
|
||||
|
||||
$statusMap = [
|
||||
'pending' => __('待结算', 'aksrt'),
|
||||
'settled' => __('已结算', 'aksrt'),
|
||||
'cancelled' => __('已取消', 'aksrt'),
|
||||
];
|
||||
$typeMap = [
|
||||
'order' => __('订单返佣', 'aksrt'),
|
||||
'invite' => __('邀请返佣', 'aksrt'),
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'order_number' => (string) ($row['order_number'] ?? ''),
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'status' => $status,
|
||||
'status_label' => $statusMap[$status] ?? $status,
|
||||
'type' => $type,
|
||||
'type_label' => $typeMap[$type] ?? $type,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'settled_at' => (string) ($row['settled_at'] ?? ''),
|
||||
'note' => (string) ($row['note'] ?? ''),
|
||||
'history_count' => count($this->get_rebate_process_log((int) ($row['id'] ?? 0))),
|
||||
'user' => $user instanceof \WP_User ? [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
] : null,
|
||||
'source_user' => $source_user instanceof \WP_User ? [
|
||||
'id' => (int) $source_user->ID,
|
||||
'display_name' => $source_user->display_name,
|
||||
'email' => $source_user->user_email,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function append_rebate_process_log(int $rebate_id, string $event, string $note = ''): void {
|
||||
$log = get_option('aksrt_rebate_process_log', []);
|
||||
if (!is_array($log)) {
|
||||
$log = [];
|
||||
}
|
||||
|
||||
$rebate_id = (int) $rebate_id;
|
||||
if (!isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
|
||||
$log[$rebate_id] = [];
|
||||
}
|
||||
|
||||
$log[$rebate_id][] = [
|
||||
'event' => sanitize_key($event),
|
||||
'note' => sanitize_textarea_field($note),
|
||||
'operator' => get_current_user_id(),
|
||||
'operator_name' => ($user = wp_get_current_user()) ? $user->display_name : '',
|
||||
'time' => current_time('mysql'),
|
||||
];
|
||||
|
||||
update_option('aksrt_rebate_process_log', $log, false);
|
||||
}
|
||||
|
||||
private function get_rebate_process_log(int $rebate_id): array {
|
||||
$log = get_option('aksrt_rebate_process_log', []);
|
||||
if (!is_array($log) || !isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values($log[$rebate_id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PermissionsController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Capability matrix
|
||||
register_rest_route(self::API_NAMESPACE, '/permissions/capabilities', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_capabilities'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/permissions/capabilities', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'save_capabilities'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_capabilities(): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return $this->error(__('能力矩阵模块不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$caps = \AKSRT\Settings\Settings::get_user_capabilities();
|
||||
$defs = \AKSRT\Settings\Settings::cap_definitions();
|
||||
$columns = \AKSRT\Settings\Settings::cap_role_columns();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'columns' => $this->get_capability_column_labels(),
|
||||
'groups' => $this->build_capability_groups($defs, $caps, $columns),
|
||||
'summary' => [
|
||||
'cap_count' => count($defs),
|
||||
'group_count' => 3,
|
||||
'column_count' => count($columns),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function save_capabilities(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return $this->error(__('能力矩阵模块不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$input = $params['user_caps'] ?? null;
|
||||
|
||||
if (!is_array($input)) {
|
||||
return $this->error(__('无效的能力矩阵数据', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
$defs = \AKSRT\Settings\Settings::cap_definitions();
|
||||
$columns = \AKSRT\Settings\Settings::cap_role_columns();
|
||||
$clean = [];
|
||||
|
||||
foreach ($defs as $cap_id => $def) {
|
||||
$row = is_array($input[$cap_id] ?? null) ? $input[$cap_id] : [];
|
||||
$clean[$cap_id] = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (in_array($column, ['level', 'vip'], true)) {
|
||||
$value = isset($row[$column]) ? (int) $row[$column] : -1;
|
||||
$max = $column === 'level' ? 10 : 2;
|
||||
$clean[$cap_id][$column] = max(-1, min($max, $value));
|
||||
} else {
|
||||
$clean[$cap_id][$column] = !empty($row[$column]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
$all['user_caps'] = $clean;
|
||||
update_option('aksrt_settings', $all);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success([
|
||||
'message' => __('能力矩阵已保存', 'aksrt'),
|
||||
'data' => [
|
||||
'user_caps' => $clean,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_capability_groups(array $defs, array $caps, array $columns): array {
|
||||
$buckets = [
|
||||
'user' => [
|
||||
'key' => 'user',
|
||||
'title' => __('用户治理', 'aksrt'),
|
||||
'items' => [],
|
||||
],
|
||||
'post' => [
|
||||
'key' => 'post',
|
||||
'title' => __('投稿权限', 'aksrt'),
|
||||
'items' => [],
|
||||
],
|
||||
'comment' => [
|
||||
'key' => 'comment',
|
||||
'title' => __('评论权限', 'aksrt'),
|
||||
'items' => [],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($defs as $cap_id => $def) {
|
||||
$bucket_key = 'user';
|
||||
if (str_starts_with($cap_id, 'comment_')) {
|
||||
$bucket_key = 'comment';
|
||||
} elseif (str_starts_with($cap_id, 'new_post_')) {
|
||||
$bucket_key = 'post';
|
||||
}
|
||||
|
||||
$excludes = $def['exclude_roles'] ?? [];
|
||||
$only_roles = $def['roles'] ?? null;
|
||||
$values = $caps[$cap_id] ?? [];
|
||||
|
||||
$visible_columns = [];
|
||||
foreach ($columns as $column) {
|
||||
if (in_array($column, $excludes, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($only_roles !== null && !in_array($column, $only_roles, true) && !in_array($column, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
|
||||
continue;
|
||||
}
|
||||
$visible_columns[] = $column;
|
||||
}
|
||||
|
||||
$buckets[$bucket_key]['items'][] = [
|
||||
'cap_id' => $cap_id,
|
||||
'label' => $def['label'],
|
||||
'visible_columns' => $visible_columns,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($buckets);
|
||||
}
|
||||
|
||||
private function get_capability_column_labels(): array {
|
||||
return [
|
||||
'all' => __('所有人', 'aksrt'),
|
||||
'logged' => __('已登录', 'aksrt'),
|
||||
'level' => __('等级≥', 'aksrt'),
|
||||
'vip' => __('VIP≥', 'aksrt'),
|
||||
'auth' => __('已认证', 'aksrt'),
|
||||
'moderator' => __('版主', 'aksrt'),
|
||||
'plate_author' => __('超级版主', 'aksrt'),
|
||||
'cat_moderator' => __('分区版主', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PostListController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
$this->register_section_routes('post-list', 'post_list');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class GlobalSchema {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'global',
|
||||
'title' => '全局&功能',
|
||||
'sections' => [
|
||||
'common' => [
|
||||
'key' => 'common',
|
||||
'title' => '常用功能',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'comment', 'title' => '评论功能', 'fields' => [
|
||||
['key' => 'comment_enabled', 'label' => '开启评论', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'performance', 'title' => '性能优化', 'fields' => [
|
||||
['key' => 'lazyload_enabled', 'label' => '图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'filing', 'title' => '备案信息', 'fields' => [
|
||||
['key' => 'icp_filing', 'label' => 'ICP 备案号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'footer_text', 'label' => '页脚附加文字', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'logo' => [
|
||||
'key' => 'logo',
|
||||
'title' => 'LOGO图像',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'logo', 'title' => 'Logo 设置', 'fields' => [
|
||||
['key' => 'site_logo', 'label' => '站点 Logo', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'site_logo_dark', 'label' => '暗色模式 Logo', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'logo_width', 'label' => 'Logo 宽度', 'type' => 'number', 'default' => 150, 'min' => 50, 'max' => 400],
|
||||
['key' => 'logo_text', 'label' => 'Logo 文字', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'favicon', 'title' => '站点图标', 'fields' => [
|
||||
['key' => 'site_favicon', 'label' => '浏览器图标', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'site_icon', 'label' => '移动端图标', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'seo' => [
|
||||
'key' => 'seo',
|
||||
'title' => 'SEO优化',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => 'SEO基础', 'fields' => [
|
||||
['key' => 'seo_enabled', 'label' => '启用核心SEO优化', 'type' => 'switch', 'default' => true, 'help' => '关闭后主题不再输出 SEO head 标签。'],
|
||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false, 'help' => '开启后文章/分类独立 SEO 框显示 AI 生成按钮;需配置 AI 连接(WP Connectors 或扩展 → AI 连接配置)。', 'dependsOn' => ['key' => 'seo_enabled', 'value' => true]],
|
||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-', 'help' => 'AI 生成标题时用于追加站点名。'],
|
||||
['key' => 'seo_title', 'label' => 'SEO标题(title)', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'seo_keywords', 'label' => 'SEO关键字(keywords)', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'seo_description', 'label' => 'SEO描述(description)', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'ai-seo', 'title' => 'AI SEO 提示词', 'fields' => [
|
||||
['key' => 'ai_seo_title_prompt', 'label' => '标题提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置 Zibll 风格默认提示词。'],
|
||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'],
|
||||
]],
|
||||
['key' => 'baidu', 'title' => '百度SEO', 'fields' => [
|
||||
['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'image', 'title' => '图片SEO', 'fields' => [
|
||||
['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'seo_image_always_default', 'label' => '固定使用默认图像', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_auto_alt', 'label' => '文章图片自动添加alt', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'redirect', 'title' => '外链重定向', 'fields' => [
|
||||
['key' => 'seo_go_link', 'label' => '启用外链重定向', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_go_link_nonce', 'label' => '外链重定向鉴权', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_go_link_post', 'label' => '文章内容外链重定向', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_go_link_exclude', 'label' => '排除域名', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'advanced', 'title' => '高级SEO', 'fields' => [
|
||||
['key' => 'seo_no_category', 'label' => '分类URL去除category', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_jsonld', 'label' => '启用JSON-LD', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_jsonld_single', 'label' => '文章页JSON-LD', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'seo_jsonld_page', 'label' => '页面JSON-LD', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'lazyload' => [
|
||||
'key' => 'lazyload',
|
||||
'title' => '懒加载设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '懒加载开关', 'fields' => [
|
||||
['key' => 'lazyload_enabled', 'label' => '图像异步懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_animation', 'label' => '懒加载动画效果', 'type' => 'select', 'default' => 'blur', 'options' => [
|
||||
['label' => '淡出淡入', 'value' => 'fade'],
|
||||
['label' => '高斯模糊', 'value' => 'blur'],
|
||||
['label' => '放大缩小', 'value' => 'scale'],
|
||||
]],
|
||||
['key' => 'lazyload_placeholder', 'label' => '懒加载预载图', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'lazyload_blur', 'label' => '模糊预览', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'scope', 'title' => '懒加载范围', 'fields' => [
|
||||
['key' => 'lazyload_avatar', 'label' => '头像懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_content', 'label' => '文章内容图片懒加载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'lazyload_comment', 'label' => '评论内容图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_private', 'label' => '私信聊天图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_slider', 'label' => '幻灯片图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_cover', 'label' => '封面图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_other', 'label' => '其它图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'lazyload_widget', 'label' => '小工具图片懒加载', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'layout' => [
|
||||
'key' => 'layout',
|
||||
'title' => '显示&布局',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'color', 'title' => '色彩方案', 'fields' => [
|
||||
['key' => 'theme_mode', 'label' => '默认主题模式', 'type' => 'radio', 'default' => 'time-auto', 'options' => [
|
||||
['label' => '浅色', 'value' => 'white-theme'],
|
||||
['label' => '深色', 'value' => 'dark-theme'],
|
||||
['label' => '跟随系统', 'value' => 'time-auto'],
|
||||
]],
|
||||
['key' => 'theme_mode_button', 'label' => '主题切换按钮位置', 'type' => 'checkbox', 'default' => ['pc_nav', 'm_menu'], 'options' => [
|
||||
['label' => 'PC 顶部导航', 'value' => 'pc_nav'],
|
||||
['label' => '移动端菜单', 'value' => 'm_menu'],
|
||||
]],
|
||||
['key' => 'theme_skin', 'label' => '预设主题色', 'type' => 'palette', 'default' => 'f04494', 'options' => [
|
||||
['label' => '玫红', 'value' => 'ff1856'],
|
||||
['label' => '粉色', 'value' => 'f04494'],
|
||||
['label' => '紫色', 'value' => 'ae53f3'],
|
||||
['label' => '靛蓝', 'value' => '627bf5'],
|
||||
['label' => '天蓝', 'value' => '00a2e3'],
|
||||
['label' => '青绿', 'value' => '16b597'],
|
||||
['label' => '翠绿', 'value' => '36af18'],
|
||||
['label' => '黄绿', 'value' => '8fb107'],
|
||||
['label' => '棕金', 'value' => 'b18c07'],
|
||||
['label' => '橘橙', 'value' => 'e06711'],
|
||||
['label' => '朱红', 'value' => 'f74735'],
|
||||
]],
|
||||
['key' => 'primary_color', 'label' => '自定义主题色', 'type' => 'color', 'default' => '#f04494'],
|
||||
['key' => 'dark_primary_color', 'label' => '暗色模式主题色', 'type' => 'color', 'default' => '#f04494'],
|
||||
['key' => 'border_radius', 'label' => '全局圆角', 'type' => 'slider', 'default' => 8, 'min' => 0, 'max' => 20],
|
||||
['key' => 'theme_main_radius', 'label' => '卡片圆角 (px)', 'type' => 'number', 'default' => 12, 'min' => 0, 'max' => 30, 'help' => '独立卡片圆角,不填则使用全局圆角 +4'],
|
||||
['key' => 'close_backdrop', 'label' => '关闭毛玻璃效果', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'layout', 'title' => '布局设置', 'fields' => [
|
||||
['key' => 'container_max_width', 'label' => '内容最大宽度', 'type' => 'number', 'default' => 1200, 'min' => 0, 'max' => 1600],
|
||||
['key' => 'sidebar_enabled', 'label' => '开启侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_position', 'label' => '侧边栏位置', 'type' => 'radio', 'default' => 'right', 'options' => [
|
||||
['label' => '左侧', 'value' => 'left'],
|
||||
['label' => '右侧', 'value' => 'right'],
|
||||
]],
|
||||
['key' => 'sidebar_width', 'label' => '侧边栏宽度', 'type' => 'number', 'default' => 300, 'min' => 200, 'max' => 400],
|
||||
['key' => 'sidebar_home_enabled', 'label' => '首页侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_category_enabled', 'label' => '分类页侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_tag_enabled', 'label' => '标签页侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_single_enabled', 'label' => '文章页侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_page_enabled', 'label' => '页面侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sidebar_search_enabled', 'label' => '搜索页侧边栏', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'typography', 'title' => '字体设置', 'fields' => [
|
||||
['key' => 'font_family', 'label' => '正文字体', 'type' => 'select', 'default' => 'system', 'options' => [
|
||||
['label' => '系统默认', 'value' => 'system'],
|
||||
['label' => '思源黑体', 'value' => 'noto-sans-sc'],
|
||||
['label' => '思源宋体', 'value' => 'noto-serif-sc'],
|
||||
['label' => '自定义', 'value' => 'custom'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'performance', 'title' => '性能优化', 'fields' => [
|
||||
['key' => 'qj_loading', 'label' => '全站加载动画', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'qj_dh_xs', 'label' => '加载动画样式', 'type' => 'select', 'default' => 'no1', 'dependsOn' => ['key' => 'qj_loading', 'value' => true], 'options' => [
|
||||
['label' => '样式 1', 'value' => 'no1'],
|
||||
['label' => '样式 2', 'value' => 'no2'],
|
||||
['label' => '样式 3', 'value' => 'no3'],
|
||||
['label' => '样式 4', 'value' => 'no4'],
|
||||
['label' => '样式 5', 'value' => 'no5'],
|
||||
['label' => '样式 6', 'value' => 'no6'],
|
||||
['label' => '样式 7', 'value' => 'no7'],
|
||||
['label' => '样式 8', 'value' => 'no8'],
|
||||
['label' => '样式 9', 'value' => 'no9'],
|
||||
['label' => '样式 10', 'value' => 'no10'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'code-highlight' => [
|
||||
'key' => 'code-highlight',
|
||||
'title' => '代码高亮',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '代码高亮设置', 'fields' => [
|
||||
['key' => 'highlight_kg', 'label' => '代码高亮开关', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'highlight_hh', 'label' => '显示行号', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'highlight_kg', 'value' => true]],
|
||||
['key' => 'highlight_btn', 'label' => '显示扩展按钮', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'highlight_kg', 'value' => true]],
|
||||
['key' => 'highlight_maxheight', 'label' => '最大高度(px,0=不限)', 'type' => 'number', 'default' => 400, 'min' => 0, 'max' => 2000, 'dependsOn' => ['key' => 'highlight_kg', 'value' => true]],
|
||||
]],
|
||||
['key' => 'theme', 'title' => '主题设置', 'fields' => [
|
||||
['key' => 'highlight_zt', 'label' => '日间默认主题', 'type' => 'select', 'default' => 'enlighter', 'dependsOn' => ['key' => 'highlight_kg', 'value' => true], 'options' => [
|
||||
['label' => '浅色: Enlighter', 'value' => 'enlighter'],
|
||||
['label' => '浅色: Bootstrap', 'value' => 'bootstrap4'],
|
||||
['label' => '浅色: Classic', 'value' => 'classic'],
|
||||
['label' => '浅色: Beyond', 'value' => 'beyond'],
|
||||
['label' => '浅色: Mowtwo', 'value' => 'mowtwo'],
|
||||
['label' => '浅色: Eclipse', 'value' => 'eclipse'],
|
||||
['label' => '浅色: Droide', 'value' => 'droide'],
|
||||
['label' => '浅色: Minimal', 'value' => 'minimal'],
|
||||
['label' => '浅色: Rowhammer', 'value' => 'rowhammer'],
|
||||
['label' => '浅色: Godzilla', 'value' => 'godzilla'],
|
||||
['label' => '深色: Dracula', 'value' => 'dracula'],
|
||||
['label' => '深色: Atomic', 'value' => 'atomic'],
|
||||
['label' => '深色: Monokai', 'value' => 'monokai'],
|
||||
]],
|
||||
['key' => 'highlight_dark_zt', 'label' => '暗色默认主题', 'type' => 'select', 'default' => 'monokai', 'dependsOn' => ['key' => 'highlight_kg', 'value' => true], 'options' => [
|
||||
['label' => '深色: Dracula', 'value' => 'dracula'],
|
||||
['label' => '深色: Atomic', 'value' => 'atomic'],
|
||||
['label' => '深色: Monokai', 'value' => 'monokai'],
|
||||
['label' => '浅色: Enlighter', 'value' => 'enlighter'],
|
||||
['label' => '浅色: Bootstrap', 'value' => 'bootstrap4'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'system-notice' => [
|
||||
'key' => 'system-notice',
|
||||
'title' => '系统公告',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '公告设置', 'fields' => [
|
||||
['key' => 'system_notice_enabled', 'label' => '启用系统公告', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'system_notice_policy', 'label' => '显示对象', 'type' => 'select', 'default' => '', 'options' => [
|
||||
['label' => '所有访客', 'value' => ''],
|
||||
['label' => '仅登录用户', 'value' => 'signin'],
|
||||
['label' => '仅 VIP 会员', 'value' => 'vip'],
|
||||
['label' => '仅非 VIP', 'value' => 'free'],
|
||||
['label' => '仅认证用户', 'value' => 'auth'],
|
||||
]],
|
||||
['key' => 'system_notice_title', 'label' => '公告标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_content', 'label' => '公告内容', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'buttons', 'title' => '按钮组(留空则不显示)', 'fields' => [
|
||||
['key' => 'system_notice_btn1_text', 'label' => '按钮 1 文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn1_link', 'label' => '按钮 1 链接', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn1_color', 'label' => '按钮 1 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
|
||||
['label' => '主题色', 'value' => 'primary'],
|
||||
['label' => '成功绿', 'value' => 'success'],
|
||||
['label' => '警告橙', 'value' => 'warning'],
|
||||
['label' => '危险红', 'value' => 'danger'],
|
||||
['label' => '信息蓝', 'value' => 'info'],
|
||||
['label' => '深紫', 'value' => 'purple'],
|
||||
['label' => '深灰', 'value' => 'dark'],
|
||||
]],
|
||||
['key' => 'system_notice_btn2_text', 'label' => '按钮 2 文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn2_link', 'label' => '按钮 2 链接', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn2_color', 'label' => '按钮 2 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
|
||||
['label' => '主题色', 'value' => 'primary'],
|
||||
['label' => '成功绿', 'value' => 'success'],
|
||||
['label' => '警告橙', 'value' => 'warning'],
|
||||
['label' => '危险红', 'value' => 'danger'],
|
||||
['label' => '信息蓝', 'value' => 'info'],
|
||||
['label' => '深紫', 'value' => 'purple'],
|
||||
['label' => '深灰', 'value' => 'dark'],
|
||||
]],
|
||||
['key' => 'system_notice_btn3_text', 'label' => '按钮 3 文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn3_link', 'label' => '按钮 3 链接', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn3_color', 'label' => '按钮 3 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
|
||||
['label' => '主题色', 'value' => 'primary'],
|
||||
['label' => '成功绿', 'value' => 'success'],
|
||||
['label' => '警告橙', 'value' => 'warning'],
|
||||
['label' => '危险红', 'value' => 'danger'],
|
||||
['label' => '信息蓝', 'value' => 'info'],
|
||||
['label' => '深紫', 'value' => 'purple'],
|
||||
['label' => '深灰', 'value' => 'dark'],
|
||||
]],
|
||||
['key' => 'system_notice_btn4_text', 'label' => '按钮 4 文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn4_link', 'label' => '按钮 4 链接', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_btn4_color', 'label' => '按钮 4 颜色', 'type' => 'select', 'default' => 'primary', 'options' => [
|
||||
['label' => '主题色', 'value' => 'primary'],
|
||||
['label' => '成功绿', 'value' => 'success'],
|
||||
['label' => '警告橙', 'value' => 'warning'],
|
||||
['label' => '危险红', 'value' => 'danger'],
|
||||
['label' => '信息蓝', 'value' => 'info'],
|
||||
['label' => '深紫', 'value' => 'purple'],
|
||||
['label' => '深灰', 'value' => 'dark'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'style', 'title' => '样式设置', 'fields' => [
|
||||
['key' => 'system_notice_size', 'label' => '弹窗尺寸', 'type' => 'select', 'default' => '', 'options' => [
|
||||
['label' => '默认', 'value' => ''],
|
||||
['label' => '迷你', 'value' => 'modal-mini'],
|
||||
['label' => '小号', 'value' => 'modal-sm'],
|
||||
['label' => '大号', 'value' => 'modal-lg'],
|
||||
]],
|
||||
['key' => 'system_notice_radius', 'label' => '按钮圆角', 'type' => 'switch', 'default' => false, 'help' => '开启后弹窗按钮使用圆角样式'],
|
||||
['key' => 'system_notice_class', 'label' => '公告样式类', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'system_notice_title_icon', 'label' => '标题图标', 'type' => 'icon', 'default' => ''],
|
||||
['key' => 'system_notice_title_class', 'label' => '标题背景配色', 'type' => 'select', 'default' => '', 'options' => [
|
||||
['label' => '无', 'value' => ''],
|
||||
['label' => '炫彩黄', 'value' => 'jb-yellow'],
|
||||
['label' => '炫彩蓝', 'value' => 'jb-blue'],
|
||||
['label' => '炫彩绿', 'value' => 'jb-green'],
|
||||
['label' => '炫彩紫', 'value' => 'jb-purple'],
|
||||
['label' => '炫彩红', 'value' => 'jb-red'],
|
||||
['label' => '炫彩橙', 'value' => 'jb-orange'],
|
||||
], 'help' => '弹窗标题区域的背景配色方案'],
|
||||
['key' => 'system_notice_title_style', 'label' => '标题样式', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'expire', 'title' => '过期设置', 'fields' => [
|
||||
['key' => 'system_notice_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 0, 'max' => 720, 'help' => '0 表示永不过期'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'search' => [
|
||||
'key' => 'search',
|
||||
'title' => '搜索功能',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '搜索基础', 'fields' => [
|
||||
['key' => 'search_mode', 'label' => '搜索模式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认搜索', 'value' => 'default'],
|
||||
['label' => 'AJAX 实时搜索', 'value' => 'ajax'],
|
||||
['label' => '跳转搜索', 'value' => 'redirect'],
|
||||
]],
|
||||
['key' => 'search_placeholder', 'label' => '搜索框提示文字', 'type' => 'input', 'default' => '开启精彩搜索'],
|
||||
]],
|
||||
['key' => 'content-types', 'title' => '搜索内容类型', 'fields' => [
|
||||
['key' => 'search_type_post', 'label' => '文章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'search_type_user', 'label' => '用户', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'search_type_category', 'label' => '分类', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'search_type_tag', 'label' => '标签', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'search_type_product', 'label' => '商品', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'search_type_topic', 'label' => '帖子', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'search_default_type', 'label' => '默认类型', 'type' => 'select', 'default' => 'post', 'options' => [
|
||||
['label' => '文章', 'value' => 'post'],
|
||||
['label' => '用户', 'value' => 'user'],
|
||||
['label' => '分类', 'value' => 'category'],
|
||||
['label' => '标签', 'value' => 'tag'],
|
||||
['label' => '商品', 'value' => 'product'],
|
||||
['label' => '帖子', 'value' => 'topic'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'features', 'title' => '搜索功能', 'fields' => [
|
||||
['key' => 'search_history_enabled', 'label' => '搜索历史', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'hot_search_enabled', 'label' => '热门搜索', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'hot_search_keywords', 'label' => '热门搜索关键词', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'search_suggestions_enabled', 'label' => '搜索建议', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'search_posts', 'label' => '搜索框热门文章', 'type' => 'switch', 'default' => true, 'help' => '在搜索面板中显示热门文章列表'],
|
||||
['key' => 'search_no_page', 'label' => '排除页面', 'type' => 'switch', 'default' => true, 'help' => '搜索结果中排除页面(仅显示文章)'],
|
||||
['key' => 'search_exclude_cats', 'label' => '排除分类', 'type' => 'textarea', 'default' => '', 'help' => '搜索结果中排除的分类 ID(逗号分隔)'],
|
||||
['key' => 'search_custom_key_s', 'label' => '自定义匹配范围', 'type' => 'switch', 'default' => true, 'help' => '启用后可配置搜索匹配字段范围(标题/内容/分类/标签等)'],
|
||||
['key' => 'search_custom_key', 'label' => '匹配字段', 'type' => 'textarea', 'default' => 'title,content', 'help' => '搜索匹配的字段范围(逗号分隔:title/content/excerpt/category/tag/author)'],
|
||||
['key' => 'search_facets', 'label' => '搜索结果筛选', 'type' => 'switch', 'default' => false, 'help' => '启用搜索结果 taxonomy 筛选面板'],
|
||||
['key' => 'search_facets_order', 'label' => '筛选排序', 'type' => 'textarea', 'default' => "category\ntag", 'help' => '筛选 fieldset 显示顺序(每行一个 taxonomy slug)'],
|
||||
]],
|
||||
['key' => 'category-search', 'title' => '分类搜索', 'fields' => [
|
||||
['key' => 'search_cat', 'label' => '启用分类搜索', 'type' => 'switch', 'default' => true, 'help' => '在搜索框旁显示分类选择器'],
|
||||
['key' => 'search_cat_in', 'label' => '默认分类', 'type' => 'input', 'default' => '', 'help' => '默认搜索的分类 ID(留空为全部分类)'],
|
||||
['key' => 'search_more_cat_obj', 'label' => '显示更多分类', 'type' => 'switch', 'default' => true, 'help' => '展开显示全部分类列表'],
|
||||
]],
|
||||
['key' => 'popular', 'title' => '热门搜索', 'fields' => [
|
||||
['key' => 'search_popular_title', 'label' => '标题', 'type' => 'input', 'default' => '热门搜索'],
|
||||
['key' => 'search_popular_key_num', 'label' => '显示数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 50],
|
||||
['key' => 'search_popular_sticky', 'label' => '置顶词(每行一个)', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'meilisearch', 'title' => 'Meilisearch 集成', 'fields' => [
|
||||
['key' => 'search_ms_enabled', 'label' => '启用 Meilisearch', 'type' => 'switch', 'default' => false, 'help' => '启用后搜索请求将发送至 Meilisearch 服务器'],
|
||||
['key' => 'search_ms_api_url', 'label' => 'API 地址', 'type' => 'input', 'default' => '', 'help' => 'Meilisearch 服务器 URL'],
|
||||
['key' => 'search_ms_api_key', 'label' => 'API Key', 'type' => 'input', 'default' => '', 'help' => 'Meilisearch Search API Key(只读密钥)'],
|
||||
['key' => 'search_ms_index_prefix', 'label' => '索引前缀', 'type' => 'input', 'default' => 'aksrt', 'help' => '索引名称前缀'],
|
||||
['key' => 'search_ms_synonyms', 'label' => '同义词(JSON)', 'type' => 'textarea', 'default' => '', 'help' => 'Meilisearch 同义词配置 JSON'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'email' => [
|
||||
'key' => 'email',
|
||||
'title' => '邮件事件',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'global', 'title' => '全局设置', 'fields' => [
|
||||
['key' => 'email_footer_text', 'label' => '邮件页脚文字', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_site_branding', 'label' => '显示站点品牌', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_template_mode', 'label' => '邮件格式', 'type' => 'radio', 'default' => 'plain', 'options' => [
|
||||
['label' => '纯文本', 'value' => 'plain'],
|
||||
['label' => 'HTML', 'value' => 'html'],
|
||||
]],
|
||||
['key' => 'email_private_receive_limit', 'label' => '私信邮件接收组', 'type' => 'select', 'default' => 'all', 'options' => [
|
||||
['label' => '所有用户', 'value' => 'all'],
|
||||
['label' => 'VIP 及以上', 'value' => 'vip'],
|
||||
['label' => 'VIP2 及以上', 'value' => 'vip2'],
|
||||
['label' => '仅管理员', 'value' => 'admin'],
|
||||
], 'help' => '限制哪些用户组可以收到私信邮件通知'],
|
||||
]],
|
||||
['key' => 'branding', 'title' => '邮件品牌', 'fields' => [
|
||||
['key' => 'mail_showname', 'label' => '发件人名称', 'type' => 'input', 'default' => '', 'help' => '覆盖系统默认发件人名称'],
|
||||
['key' => 'mail_title_prefix', 'label' => '主题前缀', 'type' => 'input', 'default' => '', 'help' => '自动添加在所有邮件主题前面,如【站点名】'],
|
||||
['key' => 'mail_description', 'label' => '邮件底部说明', 'type' => 'textarea', 'default' => '', 'help' => '附加在邮件页脚文字之后,留空不显示'],
|
||||
['key' => 'mail_more_content', 'label' => '邮件附加内容', 'type' => 'textarea', 'default' => '', 'help' => '附加在邮件内容末尾的补充说明'],
|
||||
]],
|
||||
['key' => 'reg_verify_link', 'title' => '注册验证链接', 'fields' => [
|
||||
['key' => 'email_reg_verify_link_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_reg_verify_link_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_reg_verify_link_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_reg_verify_link_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'reg_verify_code', 'title' => '注册验证码', 'fields' => [
|
||||
['key' => 'email_reg_verify_code_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_reg_verify_code_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_reg_verify_code_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_reg_verify_code_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'reg_success', 'title' => '注册成功通知', 'fields' => [
|
||||
['key' => 'email_reg_success_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_reg_success_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_reg_success_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_reg_success_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'password_reset', 'title' => '密码重置', 'fields' => [
|
||||
['key' => 'email_password_reset_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_password_reset_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_password_reset_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_password_reset_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'change_code', 'title' => '邮箱变更验证码', 'fields' => [
|
||||
['key' => 'email_change_code_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_change_code_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_change_code_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_change_code_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'change_notice', 'title' => '邮箱变更通知', 'fields' => [
|
||||
['key' => 'email_change_notice_enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'email_change_notice_subject', 'label' => '主题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'email_change_notice_content', 'label' => '内容模板', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'email_change_notice_html', 'label' => 'HTML 格式', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'custom-code' => [
|
||||
'key' => 'custom-code',
|
||||
'title' => '自定义代码',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'css', 'title' => '自定义 CSS', 'fields' => [
|
||||
['key' => 'custom_css', 'label' => '自定义 CSS', 'type' => 'code', 'language' => 'css', 'default' => ''],
|
||||
]],
|
||||
['key' => 'code', 'title' => '头部 & 底部代码', 'fields' => [
|
||||
['key' => 'head_code', 'label' => '头部 HTML 代码', 'type' => 'code', 'default' => '', 'help' => '在 <head> 中输出,支持 meta/link/style/script 等任意 HTML'],
|
||||
['key' => 'foot_code', 'label' => '底部 HTML 代码', 'type' => 'code', 'default' => '', 'help' => '在 </body> 前输出,支持任意 HTML'],
|
||||
]],
|
||||
['key' => 'stats', 'title' => '统计代码', 'fields' => [
|
||||
['key' => 'trackcode', 'label' => '统计代码', 'type' => 'code', 'default' => '', 'help' => '百度统计/GA/CNZZ/51la 等,自动在 head 中输出'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'i18n' => [
|
||||
'key' => 'i18n',
|
||||
'title' => '多语言翻译',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础配置', 'fields' => [
|
||||
['key' => 'i18n_enabled', 'label' => '启用多语言', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'i18n_default_lang', 'label' => '默认语言', 'type' => 'select', 'default' => 'zh-CN', 'options' => [
|
||||
['label' => '简体中文', 'value' => 'zh-CN'],
|
||||
['label' => 'English', 'value' => 'en'],
|
||||
['label' => '繁體中文', 'value' => 'zh-TW'],
|
||||
['label' => '日本語', 'value' => 'ja'],
|
||||
['label' => '한국어', 'value' => 'ko'],
|
||||
['label' => 'Français', 'value' => 'fr'],
|
||||
['label' => 'Deutsch', 'value' => 'de'],
|
||||
['label' => 'Español', 'value' => 'es'],
|
||||
['label' => 'Русский', 'value' => 'ru'],
|
||||
]],
|
||||
['key' => 'i18n_auto_detect', 'label' => '自动检测浏览器语言', 'type' => 'switch', 'default' => true, 'help' => '基于浏览器 Accept-Language 自动切换(AKSRT 方案,区别于 zibll IP 定位方案)'],
|
||||
]],
|
||||
['key' => 'languages', 'title' => '语言管理', 'fields' => [
|
||||
['key' => 'i18n_available_langs', 'label' => '可用语言列表', 'type' => 'textarea', 'default' => "zh-CN|简体中文\nen|English\nzh-TW|繁體中文\nja|日本語\nko|한국어\nfr|Français\nde|Deutsch\nes|Español\nru|Русский", 'help' => '每行一个语言,格式:代码|名称(9 种内置语言)'],
|
||||
['key' => 'i18n_nomenclature', 'label' => '自定义术语', 'type' => 'code', 'default' => '', 'help' => 'JSON 格式自定义翻译术语(key => 翻译),对齐 zibll nomenclature'],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'i18n_switcher_position', 'label' => '语言切换器位置', 'type' => 'radio', 'default' => 'footer', 'options' => [
|
||||
['label' => '导航栏', 'value' => 'header'],
|
||||
['label' => '页脚', 'value' => 'footer'],
|
||||
['label' => '导航栏+页脚', 'value' => 'both'],
|
||||
['label' => '隐藏', 'value' => 'none'],
|
||||
]],
|
||||
['key' => 'i18n_url_strategy', 'label' => 'URL 策略', 'type' => 'radio', 'default' => 'query', 'options' => [
|
||||
['label' => '查询参数', 'value' => 'query'],
|
||||
['label' => '路径前缀', 'value' => 'path'],
|
||||
['label' => '子域名', 'value' => 'subdomain'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema\PostList;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPostListList {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'list',
|
||||
'title' => '文章列表',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
[
|
||||
'key' => 'order',
|
||||
'title' => '默认排序与置顶',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'list_orderby',
|
||||
'label' => '默认排序方式',
|
||||
'type' => 'radio',
|
||||
'default' => 'modified',
|
||||
'options' => [
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '更新时间', 'value' => 'modified'],
|
||||
['label' => 'WP默认', 'value' => 'default'],
|
||||
],
|
||||
'help' => '文章列表全局默认排序(对齐 Zibll list_orderby)',
|
||||
],
|
||||
[
|
||||
'key' => 'sticky_s',
|
||||
'label' => '显示置顶文章',
|
||||
'type' => 'checkbox',
|
||||
'default' => ['home', 'cat'],
|
||||
'options' => [
|
||||
['label' => '首页', 'value' => 'home'],
|
||||
['label' => '分类页', 'value' => 'cat'],
|
||||
['label' => '标签页', 'value' => 'tag'],
|
||||
['label' => '专题页', 'value' => 'topics'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'display',
|
||||
'title' => '列表显示',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'target_blank',
|
||||
'label' => '新窗口打开文章',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'key' => 'item_heading_bold',
|
||||
'label' => '列表标题粗体显示',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'key' => 'post_list_author',
|
||||
'label' => '显示文章作者',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'post_list_time',
|
||||
'label' => '显示文章时间',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
'help' => '建议作者与时间至少开启一项',
|
||||
],
|
||||
[
|
||||
'key' => 'list_meta_show',
|
||||
'label' => '列表小部件(移动端优先)',
|
||||
'type' => 'radio',
|
||||
'default' => 'like',
|
||||
'options' => [
|
||||
['label' => '阅读量', 'value' => 'view'],
|
||||
['label' => '点赞数', 'value' => 'like'],
|
||||
['label' => '评论', 'value' => 'comm'],
|
||||
],
|
||||
'help' => '移动端空间不足时始终保留的部件',
|
||||
],
|
||||
[
|
||||
'key' => 'list_badge_show',
|
||||
'label' => '列表徽章',
|
||||
'type' => 'checkbox',
|
||||
'default' => ['pay', 'tag', 'topics', 'cat'],
|
||||
'options' => [
|
||||
['label' => '付费', 'value' => 'pay'],
|
||||
['label' => '分类', 'value' => 'cat'],
|
||||
['label' => '专题', 'value' => 'topics'],
|
||||
['label' => '标签', 'value' => 'tag'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'paging',
|
||||
'title' => '列表翻页',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'paging_ajax_s',
|
||||
'label' => '列表翻页模式',
|
||||
'type' => 'radio',
|
||||
'default' => '1',
|
||||
'options' => [
|
||||
['label' => 'AJAX 追加列表翻页', 'value' => '1'],
|
||||
['label' => '数字翻页按钮', 'value' => '0'],
|
||||
],
|
||||
'help' => '对齐 Zibll paging_ajax_s;单页数量仍受 WP 阅读设置影响',
|
||||
],
|
||||
[
|
||||
'key' => 'paging_ajax_ias_s',
|
||||
'label' => 'AJAX 自动加载',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
'dependsOn' => ['key' => 'paging_ajax_s', 'value' => '1'],
|
||||
'help' => '滚动到列表尽头时自动加载下一页(Zibll paging_ajax_ias_s)',
|
||||
],
|
||||
[
|
||||
'key' => 'ias_max',
|
||||
'label' => '自动加载页数',
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
'min' => 0,
|
||||
'max' => 10,
|
||||
'dependsOn' => ['key' => 'paging_ajax_s', 'value' => '1'],
|
||||
'help' => '0 表示不限制,直到加载完全部列表',
|
||||
],
|
||||
[
|
||||
'key' => 'ajax_trigger',
|
||||
'label' => '加载更多文案',
|
||||
'type' => 'input',
|
||||
'default' => '加载更多',
|
||||
'dependsOn' => ['key' => 'paging_ajax_s', 'value' => '1'],
|
||||
],
|
||||
[
|
||||
'key' => 'ajax_nomore',
|
||||
'label' => '没有更多文案',
|
||||
'type' => 'input',
|
||||
'default' => '没有更多了',
|
||||
'dependsOn' => ['key' => 'paging_ajax_s', 'value' => '1'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'card',
|
||||
'title' => '卡片与布局',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'post_card_style',
|
||||
'label' => '默认列表模式',
|
||||
'type' => 'select',
|
||||
'default' => 'thumb',
|
||||
'options' => [
|
||||
['label' => '列表图文模式', 'value' => 'thumb'],
|
||||
['label' => '紧凑卡', 'value' => 'compact'],
|
||||
['label' => '大图卡', 'value' => 'large_thumb'],
|
||||
['label' => '多图卡', 'value' => 'multi_thumb'],
|
||||
['label' => '列表文字模式', 'value' => 'text'],
|
||||
['label' => '列表自动图文', 'value' => 'thumb_if_has'],
|
||||
['label' => '卡片模式', 'value' => 'card'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'post_excerpt_length',
|
||||
'label' => '摘要长度',
|
||||
'type' => 'number',
|
||||
'default' => 120,
|
||||
'min' => 20,
|
||||
'max' => 500,
|
||||
],
|
||||
[
|
||||
'key' => 'post_list_per_page',
|
||||
'label' => '每页文章数',
|
||||
'type' => 'number',
|
||||
'default' => 12,
|
||||
'min' => 4,
|
||||
'max' => 48,
|
||||
],
|
||||
[
|
||||
'key' => 'list_show_type',
|
||||
'label' => '列表间距样式',
|
||||
'type' => 'radio',
|
||||
'default' => 'separate',
|
||||
'options' => [
|
||||
['label' => '分隔卡片', 'value' => 'separate'],
|
||||
['label' => '无间距', 'value' => 'no_margin'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'list_list_style',
|
||||
'label' => '列表样式变体',
|
||||
'type' => 'radio',
|
||||
'default' => '',
|
||||
'options' => [
|
||||
['label' => '默认', 'value' => ''],
|
||||
['label' => '样式2', 'value' => 'style2'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'list_list_img_position',
|
||||
'label' => '列表缩略图位置',
|
||||
'type' => 'radio',
|
||||
'default' => 'left',
|
||||
'options' => [
|
||||
['label' => '左侧', 'value' => 'left'],
|
||||
['label' => '右侧', 'value' => 'right'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'list_list_scale',
|
||||
'label' => '列表缩略图比例 (%)',
|
||||
'type' => 'number',
|
||||
'default' => 70,
|
||||
'min' => 50,
|
||||
'max' => 200,
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_style',
|
||||
'label' => '卡片样式变体',
|
||||
'type' => 'radio',
|
||||
'default' => '',
|
||||
'options' => [
|
||||
['label' => '默认', 'value' => ''],
|
||||
['label' => '样式3', 'value' => 'style3'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_scale',
|
||||
'label' => '卡片缩略图比例 (%)',
|
||||
'type' => 'number',
|
||||
'default' => 70,
|
||||
'min' => 20,
|
||||
'max' => 200,
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_home',
|
||||
'label' => '首页卡片模式',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_cat',
|
||||
'label' => '分类卡片模式',
|
||||
'type' => 'taxonomy_select',
|
||||
'taxonomy' => 'category',
|
||||
'multiple' => true,
|
||||
'default' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_tag',
|
||||
'label' => '标签页卡片模式',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_topics',
|
||||
'label' => '专题页卡片模式',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'key' => 'list_card_author',
|
||||
'label' => '用户页卡片模式',
|
||||
'type' => 'switch',
|
||||
'default' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensions {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'extensions',
|
||||
'title' => '扩展&增强',
|
||||
'sections' => [
|
||||
'ip' => SchemaExtensionsIP::build(),
|
||||
'audit' => SchemaExtensionsAudit::build(),
|
||||
'push' => SchemaExtensionsPush::build(),
|
||||
'security' => SchemaExtensionsSecurity::build(),
|
||||
'docs-mode' => SchemaExtensionsDocsMode::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsAudit {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'audit',
|
||||
'title' => 'API内容审核',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'provider', 'title' => '审核接口', 'fields' => [
|
||||
['key' => 'content_audit_enabled', 'label' => '启用内容审核', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'content_audit_provider', 'label' => '审核服务商', 'type' => 'radio', 'default' => 'baidu', 'options' => [
|
||||
['label' => '百度内容审核', 'value' => 'baidu'],
|
||||
['label' => '关闭审核接口', 'value' => 'null'],
|
||||
]],
|
||||
['key' => 'content_audit_api_key', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'content_audit_api_secret', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'scope', 'title' => '审核范围', 'fields' => [
|
||||
['key' => 'content_audit_text_enabled', 'label' => '文本审核', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'content_audit_image_enabled', 'label' => '图片审核', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'fallback', 'title' => '失败策略', 'fields' => [
|
||||
['key' => 'content_audit_on_failure', 'label' => '审核失败时', 'type' => 'radio', 'default' => 'allow', 'options' => [
|
||||
['label' => '放行', 'value' => 'allow'],
|
||||
['label' => '拦截', 'value' => 'block'],
|
||||
]],
|
||||
['key' => 'content_audit_log_enabled', 'label' => '记录审核日志', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsDocsMode {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'docs-mode',
|
||||
'title' => '文档模式',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础配置', 'fields' => [
|
||||
['key' => 'docs_mode_enabled', 'label' => '启用文档模式', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'docs_mode_default_template', 'label' => '文档首页模板', 'type' => 'select', 'default' => 'documentnav', 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true], 'options' => [
|
||||
['label' => '文档导航页', 'value' => 'documentnav'],
|
||||
['label' => '专题聚合页', 'value' => 'special'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'navigation', 'title' => '导航设置', 'fields' => [
|
||||
['key' => 'docs_mode_toc_enabled', 'label' => '显示目录树', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
['key' => 'docs_mode_chapter_nav_enabled', 'label' => '显示章节导航', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
['key' => 'docs_mode_default_order', 'label' => '默认排序', 'type' => 'radio', 'default' => 'menu_order', 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true], 'options' => [
|
||||
['label' => '按菜单顺序', 'value' => 'menu_order'],
|
||||
['label' => '按发布时间', 'value' => 'date'],
|
||||
['label' => '按标题排序', 'value' => 'title'],
|
||||
]],
|
||||
['key' => 'docs_mode_toc_depth', 'label' => '目录深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 6, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
['key' => 'docs_mode_chapter_title', 'label' => '章节标题', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'filter', 'title' => '筛选设置', 'fields' => [
|
||||
['key' => 'docs_mode_cats', 'label' => '文档分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
['key' => 'docs_mode_exclude', 'label' => '在首页排除此类内容', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'style', 'title' => '样式设置', 'fields' => [
|
||||
['key' => 'docs_mode_style', 'label' => '文档样式', 'type' => 'select', 'default' => 'default', 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true], 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '简洁', 'value' => 'simple'],
|
||||
['label' => '卡片', 'value' => 'card'],
|
||||
]],
|
||||
['key' => 'docs_mode_icon', 'label' => '显示图标', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
['key' => 'docs_mode_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'docs_mode_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsIP {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'ip',
|
||||
'title' => 'IP归属地',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础配置', 'fields' => [
|
||||
['key' => 'ip_geo_enabled', 'label' => '启用 IP 归属地', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'ip_addr_sdk', 'label' => 'IP 接口服务商', 'type' => 'radio', 'default' => 'null', 'options' => [
|
||||
['label' => '轮流查询', 'value' => 'polling'],
|
||||
['label' => '腾讯位置服务', 'value' => 'qq'],
|
||||
['label' => '高德位置服务', 'value' => 'amap'],
|
||||
['label' => '太平洋公共接口', 'value' => 'null'],
|
||||
]],
|
||||
['key' => 'ip_geo_cache_ttl', 'label' => '缓存时间 (秒)', 'type' => 'number', 'default' => 86400, 'min' => 3600, 'max' => 604800],
|
||||
]],
|
||||
['key' => 'providers', 'title' => '接口凭据', 'fields' => [
|
||||
['key' => 'ip_addr_sdk_qq_appkey', 'label' => '腾讯 AppKey', 'type' => 'input', 'default' => '', 'help' => '「腾讯位置服务」或「轮流查询」时填写'],
|
||||
['key' => 'ip_addr_sdk_qq_secretkey', 'label' => '腾讯 SecretKey', 'type' => 'input', 'default' => '', 'help' => '启用 SK 签名时必填'],
|
||||
['key' => 'ip_addr_sdk_amap_appkey', 'label' => '高德 AppKey', 'type' => 'input', 'default' => '', 'help' => '须为「Web服务」Key,并开通 IP 定位'],
|
||||
['key' => 'ip_addr_sdk_amap_secretkey', 'label' => '高德 SecretKey', 'type' => 'input', 'default' => '', 'help' => '启用数字签名时必填'],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'ip_geo_display', 'label' => '展示位置', 'type' => 'checkbox', 'default' => ['comment', 'user_home', 'user_center', 'post'], 'sanitize_type' => 'array', 'options' => [
|
||||
['label' => '评论区', 'value' => 'comment'],
|
||||
['label' => '用户主页', 'value' => 'user_home'],
|
||||
['label' => '用户中心', 'value' => 'user_center'],
|
||||
['label' => '文章详细页', 'value' => 'post'],
|
||||
]],
|
||||
['key' => 'ip_geo_format', 'label' => '显示格式', 'type' => 'radio', 'default' => 'province_city', 'options' => [
|
||||
['label' => '省份+城市', 'value' => 'province_city'],
|
||||
['label' => '仅省份', 'value' => 'province'],
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'ip_geo_fallback_text', 'label' => '查询失败时文案', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsPush {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'push',
|
||||
'title' => '百度推送',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '推送配置', 'fields' => [
|
||||
['key' => 'baidu_push_enabled', 'label' => '启用百度推送', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'baidu_push_token', 'label' => '推送 Token', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'trigger', 'title' => '触发规则', 'fields' => [
|
||||
['key' => 'baidu_push_auto', 'label' => '自动推送', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [
|
||||
['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'advanced', 'title' => '高级设置', 'fields' => [
|
||||
['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
||||
['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsSecurity {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'security',
|
||||
'title' => '网站安全',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础安全', 'fields' => [
|
||||
['key' => 'hide_admin_bar', 'label' => '前台关闭顶部 admin_bar', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'remove_emoji', 'label' => '移除Emoji脚本', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'remove_open_sans', 'label' => '移除Open Sans字体', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'disabled_pingback', 'label' => '禁用Pingback', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'disabled_xmlrpc', 'label' => '禁用XML-RPC', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'close_attachment_page', 'label' => '关闭附件页面', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_disable_admin', 'label' => '非管理员禁止进入后台', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'upload', 'title' => '上传设置', 'fields' => [
|
||||
['key' => 'split_upload_s', 'label' => '分片上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'split_minimum_size', 'label' => '最小分片大小(MB)', 'type' => 'number', 'default' => 20, 'min' => 4, 'max' => 100, 'step' => 2],
|
||||
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'options' => [
|
||||
['label' => '随机字符串', 'value' => 'random'],
|
||||
['label' => '时间+原文件名', 'value' => 'time'],
|
||||
]],
|
||||
['key' => 'disabled_large_thumbnail', 'label' => '禁用大缩略图', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'admin', 'title' => '后台设置', 'fields' => [
|
||||
['key' => 'remove_dashboard_widgets', 'label' => 'WP 后台加速:移除站点健康/活动/禁止浏览器&PHP版本检测', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'display_wp_update', 'label' => '禁止 WP 检测更新', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'close_gutenberg', 'label' => '关闭古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '禁用后编辑器增强全部失效'],
|
||||
['key' => 'disabled_autoembed', 'label' => '禁用自动嵌入', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'mce_float_toolbar', 'label' => '浮动工具栏', 'type' => 'checkbox', 'default' => ['pc_s', 'm_s'], 'sanitize_type' => 'array', 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc_s'],
|
||||
['label' => '移动端', 'value' => 'm_s'],
|
||||
]],
|
||||
['key' => 'admin_user_del_fields', 'label' => '关闭后台用户编辑多余选项', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'admin_frontend_set', 'label' => '前台设置入口', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'format', 'title' => '格式设置', 'fields' => [
|
||||
['key' => 'time_ago_s', 'label' => '时间格式', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'time_format', 'label' => '时间显示格式', 'type' => 'input', 'default' => 'n月j日 H:i'],
|
||||
['key' => 'remove_more_wp_head', 'label' => '移除WP head多余代码', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'captcha-note', 'title' => '人机验证', 'fields' => [
|
||||
['key' => 'captcha_redirect_hint', 'label' => '配置入口', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '人机验证(腾讯云 / 阿里云 / Cloudflare / 极验)请到「功能&权限 → 人机验证」统一配置;本页不再维护验证码字段。'],
|
||||
]],
|
||||
['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [
|
||||
['key' => 'brush_limit_post_enabled', 'label' => '文章发布限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'brush_limit_post_minutes_10', 'label' => '文章 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
|
||||
['key' => 'brush_limit_post_day_1', 'label' => '文章每日限制', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 2000],
|
||||
['key' => 'brush_limit_comment_enabled', 'label' => '评论频率限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'brush_limit_comment_minutes_10', 'label' => '评论 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
|
||||
['key' => 'brush_limit_comment_day_1', 'label' => '评论每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
|
||||
['key' => 'brush_limit_upload_enabled', 'label' => '上传频率限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'brush_limit_upload_minutes_10', 'label' => '上传 10 分钟限制', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
|
||||
['key' => 'brush_limit_upload_day_1', 'label' => '上传每日限制', 'type' => 'number', 'default' => 200, 'min' => 1, 'max' => 2000],
|
||||
['key' => 'brush_limit_register_enabled', 'label' => '注册频率限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'brush_limit_register_minutes_10', 'label' => '注册 10 分钟限制', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
|
||||
['key' => 'brush_limit_register_day_1', 'label' => '注册每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
|
||||
['key' => 'brush_limit_order_enabled', 'label' => '订单频率限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'brush_limit_order_minutes_10', 'label' => '订单 10 分钟限制', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 200],
|
||||
['key' => 'brush_limit_order_day_1', 'label' => '订单每日限制', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 2000],
|
||||
['key' => 'error_frequency_limit', 'label' => '错误频率限制', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'content-audit', 'title' => '内容审核', 'fields' => [
|
||||
['key' => 'audit_comment', 'label' => '审核评论', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_new_post', 'label' => '审核新文章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_upload_img', 'label' => '审核上传图片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_user_desc', 'label' => '审核用户简介', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_msg_private', 'label' => '审核私信', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_be_like_text', 'label' => '文本允许疑似合规', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_be_like_img', 'label' => '图片允许疑似合规', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPageDisplay {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'page_display',
|
||||
'title' => '页面&显示',
|
||||
'sections' => [
|
||||
'header-nav' => [
|
||||
'key' => 'header-nav',
|
||||
'title' => '顶部导航栏',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'layout', 'title' => '导航栏布局', 'fields' => [
|
||||
['key' => 'header_fixed', 'label' => '固定导航栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_nav_style', 'label' => '导航栏风格', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '透明', 'value' => 'transparent'],
|
||||
['label' => '毛玻璃', 'value' => 'blur'],
|
||||
]],
|
||||
['key' => 'header_max_width', 'label' => '导航栏最大宽度', 'type' => 'number', 'default' => 1200, 'min' => 0, 'max' => 1600],
|
||||
['key' => 'header_scroll_down_hide', 'label' => '下滑隐藏导航栏', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'layout-type', 'title' => '布局类型', 'fields' => [
|
||||
['key' => 'header_layout', 'label' => '桌面端布局', 'type' => 'image_select', 'default' => '1', 'options' => self::header_layout_options()],
|
||||
['key' => 'mobile_header_layout', 'label' => '移动端布局', 'type' => 'image_select', 'default' => 'center', 'options' => self::mobile_header_layout_options()],
|
||||
['key' => 'mobile_navbar_align', 'label' => '移动端菜单方向', 'type' => 'radio', 'default' => 'left', 'options' => [
|
||||
['label' => '顶部(关闭手势)', 'value' => 'top'],
|
||||
['label' => '左侧滑出', 'value' => 'left'],
|
||||
['label' => '右侧滑出', 'value' => 'right'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'components', 'title' => '导航组件', 'fields' => [
|
||||
['key' => 'header_search_enabled', 'label' => '搜索入口', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_user_menu', 'label' => '用户菜单', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_mobile_drawer', 'label' => '移动端抽屉导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_nav_pay_vip', 'label' => '导航栏开通会员按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_nav_user_pay_vip', 'label' => '用户框开通会员按钮', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'newposts', 'title' => '发布按钮', 'fields' => [
|
||||
['key' => 'header_newposts_enabled', 'label' => '启用发布按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_newposts_text', 'label' => '按钮文案', 'type' => 'input', 'default' => '<i class="fa fa-pencil"></i> 发布'],
|
||||
['key' => 'header_newposts_radius', 'label' => '按钮圆角(胶囊)', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_newposts_class', 'label' => '按钮颜色', 'type' => 'select', 'default' => 'jb-blue', 'options' => [
|
||||
['label' => '蓝色', 'value' => 'jb-blue'],
|
||||
['label' => '红色', 'value' => 'jb-red'],
|
||||
['label' => '绿色', 'value' => 'jb-green'],
|
||||
['label' => '黄色', 'value' => 'jb-yellow'],
|
||||
['label' => '紫色', 'value' => 'jb-purple'],
|
||||
['label' => '粉色', 'value' => 'jb-pink'],
|
||||
]],
|
||||
['key' => 'header_newposts_types', 'label' => '发布类型', 'type' => 'checkbox', 'default' => ['post'], 'options' => [
|
||||
['label' => '文章', 'value' => 'post'],
|
||||
['label' => '帖子', 'value' => 'topic'],
|
||||
['label' => '商品', 'value' => 'product'],
|
||||
['label' => '版块', 'value' => 'plate'],
|
||||
]],
|
||||
['key' => 'header_user_newposts_btn', 'label' => '用户卡发布类型', 'type' => 'select', 'default' => 'post', 'options' => [
|
||||
['label' => '文章', 'value' => 'post'],
|
||||
['label' => '帖子', 'value' => 'topic'],
|
||||
['label' => '商品', 'value' => 'product'],
|
||||
['label' => '版块', 'value' => 'plate'],
|
||||
]],
|
||||
['key' => 'header_user_newposts_text', 'label' => '用户卡发布文案', 'type' => 'input', 'default' => '发布内容'],
|
||||
]],
|
||||
['key' => 'color', 'title' => '颜色设置', 'fields' => [
|
||||
['key' => 'header_theme_custom', 'label' => '自定义导航栏颜色', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'header_theme_bg_custom', 'label' => '自定义背景色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'header_theme_bg', 'label' => '背景颜色(备选)', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'header_theme_color_custom', 'label' => '自定义文字颜色', 'type' => 'color', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'header-components' => [
|
||||
'key' => 'header-components',
|
||||
'title' => '顶部多功能组件',
|
||||
'description' => '配置顶部组件的显示范围、设备规则、轮播参数、搜索覆盖层与图标卡片。幻灯片图片/文案内容请到「Hero 轮播」「Banner 轮播」管理。',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'visibility', 'title' => '显示规则', 'fields' => [
|
||||
['key' => 'header_components_search_enabled', 'label' => '启用搜索覆盖层', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_components_cards_enabled', 'label' => '启用图标卡片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'header_components_show_on', 'label' => '按页面类型启用', 'type' => 'checkbox', 'default' => ['home'], 'options' => [
|
||||
['label' => '首页', 'value' => 'home'],
|
||||
['label' => '专题页', 'value' => 'topics'],
|
||||
['label' => '分类页', 'value' => 'category'],
|
||||
['label' => '标签页', 'value' => 'tag'],
|
||||
['label' => '用户页', 'value' => 'author'],
|
||||
['label' => '文章页', 'value' => 'single'],
|
||||
['label' => '搜索页', 'value' => 'search'],
|
||||
['label' => '其它页面', 'value' => 'page'],
|
||||
]],
|
||||
['key' => 'header_components_device_visibility', 'label' => '设备显示规则', 'type' => 'radio', 'default' => '', 'options' => [
|
||||
['label' => '全部', 'value' => ''],
|
||||
['label' => '仅电脑', 'value' => 'only_pc'],
|
||||
['label' => '仅移动', 'value' => 'only_sm'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'nav-color', 'title' => '导航栏配色', 'fields' => [
|
||||
['key' => 'header_components_nav_color', 'label' => '导航栏文字颜色', 'type' => 'color', 'default' => '#ffffff'],
|
||||
]],
|
||||
['key' => 'slider-config', 'title' => '幻灯片配置', 'fields' => [
|
||||
['key' => 'slider_config_direction', 'label' => '滑动方向', 'type' => 'radio', 'default' => 'horizontal', 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []], 'options' => [
|
||||
['label' => '水平', 'value' => 'horizontal'],
|
||||
['label' => '垂直', 'value' => 'vertical'],
|
||||
]],
|
||||
['key' => 'slider_config_speed', 'label' => '切换速度(ms)', 'type' => 'number', 'default' => 300, 'min' => 100, 'max' => 3000, 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
['key' => 'slider_config_button', 'label' => '显示前进后退按钮', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
['key' => 'slider_config_scale_height', 'label' => '缩放高度', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
['key' => 'slider_config_scale', 'label' => '缩放效果', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
['key' => 'slider_config_max_height', 'label' => '最大高度', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
['key' => 'slider_config_min_height', 'label' => '最小高度', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'header_components_show_on', 'not' => []]],
|
||||
]],
|
||||
['key' => 'search', 'title' => '搜索组件', 'fields' => [
|
||||
['key' => 'header_components_search_pc', 'label' => 'PC 端显示搜索', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true]],
|
||||
['key' => 'header_components_search_mobile', 'label' => '移动端显示搜索', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true]],
|
||||
['key' => 'header_components_search_show_keywords', 'label' => '热门搜索', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true]],
|
||||
['key' => 'header_components_search_popular_limit', 'label' => '关键词最多显示', 'type' => 'number', 'default' => 6, 'min' => 2, 'max' => 50, 'dependsOn' => ['key' => 'header_components_search_show_keywords', 'value' => true]],
|
||||
['key' => 'header_components_search_show_input_cat', 'label' => '显示分类搜索', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true]],
|
||||
['key' => 'header_components_search_show_type', 'label' => '显示类型切换', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true]],
|
||||
[
|
||||
'key' => 'header_components_search_before_html',
|
||||
'label' => '搜索上方额外内容',
|
||||
'type' => 'textarea',
|
||||
'sanitize_type' => 'html',
|
||||
'default' => '<div class="em16 font-bold mb10">更优雅的Wordpress主题</div>
|
||||
<div>这是导航栏幻灯片多功能组件,可在后台进行配置</div>',
|
||||
'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true],
|
||||
'help' => 'Hero 主标题/副标题写在这里(支持 HTML)。勿依赖幻灯片文案,以免与搜索重叠;请注意组件总高度。',
|
||||
],
|
||||
[
|
||||
'key' => 'header_components_search_after_html',
|
||||
'label' => '搜索下方额外内容',
|
||||
'type' => 'textarea',
|
||||
'sanitize_type' => 'html',
|
||||
'default' => '',
|
||||
'dependsOn' => ['key' => 'header_components_search_enabled', 'value' => true],
|
||||
'help' => '支持 HTML;请注意组件总高度。',
|
||||
],
|
||||
]],
|
||||
['key' => 'cards', 'title' => '图标卡片样式', 'fields' => [
|
||||
['key' => 'header_components_filter_blur_devices', 'label' => '组件高斯模糊特效', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'header_components_card_show_devices', 'label' => '卡片显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'dependsOn' => ['key' => 'header_components_cards_enabled', 'value' => true], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'header_components_card_shared_bg', 'label' => '共享卡片背景', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'header_components_cards_enabled', 'value' => true]],
|
||||
['key' => 'header_components_card_icon_shape', 'label' => '图标形状', 'type' => 'radio', 'default' => 'circle', 'dependsOn' => ['key' => 'header_components_cards_enabled', 'value' => true], 'options' => [
|
||||
['label' => '圆形', 'value' => 'circle'],
|
||||
['label' => '方形', 'value' => 'square'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'icon-cards', 'title' => '图标卡片列表', 'fields' => [
|
||||
['key' => 'header_components_icon_cards', 'label' => '图标卡片', 'type' => 'repeater', 'sanitize_type' => 'array', 'default' => [], 'dependsOn' => ['key' => 'header_components_cards_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'category' => [
|
||||
'key' => 'category',
|
||||
'title' => '分类页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'header', 'title' => '分类头图', 'fields' => [
|
||||
['key' => 'category_header_style', 'label' => '头部风格', 'type' => 'radio', 'default' => 'cover', 'options' => [
|
||||
['label' => '封面图', 'value' => 'cover'],
|
||||
['label' => '简洁标题', 'value' => 'simple'],
|
||||
['label' => '隐藏', 'value' => 'none'],
|
||||
]],
|
||||
['key' => 'category_cover_enabled', 'label' => '显示封面图', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'category_header_style', 'value' => 'cover']],
|
||||
['key' => 'category_desc_enabled', 'label' => '显示分类简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'category_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
['key' => 'list', 'title' => '列表设置', 'fields' => [
|
||||
['key' => 'category_list_mode', 'label' => '列表模式', 'type' => 'radio', 'default' => 'grid', 'options' => [
|
||||
['label' => '网格', 'value' => 'grid'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
['label' => '紧凑', 'value' => 'compact'],
|
||||
]],
|
||||
['key' => 'category_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
['key' => 'category_filter_bar', 'label' => '筛选条', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'category_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'category_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'orderby', 'title' => '排序设置', 'fields' => [
|
||||
['key' => 'category_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'modified', 'options' => [
|
||||
['label' => '最近更新', 'value' => 'modified'],
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '浏览数', 'value' => 'views'],
|
||||
['label' => '评论数', 'value' => 'comments'],
|
||||
['label' => '点赞数', 'value' => 'likes'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'tag' => [
|
||||
'key' => 'tag',
|
||||
'title' => '标签页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'header', 'title' => '标签头图', 'fields' => [
|
||||
['key' => 'tag_header_style', 'label' => '头部风格', 'type' => 'radio', 'default' => 'simple', 'options' => [
|
||||
['label' => '简洁标题', 'value' => 'simple'],
|
||||
['label' => '封面图', 'value' => 'cover'],
|
||||
['label' => '隐藏', 'value' => 'none'],
|
||||
]],
|
||||
['key' => 'tag_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
['key' => 'list', 'title' => '列表设置', 'fields' => [
|
||||
['key' => 'tag_list_mode', 'label' => '列表模式', 'type' => 'radio', 'default' => 'grid', 'options' => [
|
||||
['label' => '网格', 'value' => 'grid'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
]],
|
||||
['key' => 'tag_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
['key' => 'tag_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'tag_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'orderby', 'title' => '排序设置', 'fields' => [
|
||||
['key' => 'tag_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'count', 'options' => [
|
||||
['label' => '文章数', 'value' => 'count'],
|
||||
['label' => '名称', 'value' => 'name'],
|
||||
['label' => '最近更新', 'value' => 'modified'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'user-home' => [
|
||||
'key' => 'user-home',
|
||||
'title' => '用户主页',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'layout', 'title' => '主页布局', 'fields' => [
|
||||
['key' => 'user_home_cover', 'label' => '封面区域', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_sidebar', 'label' => '侧边栏(已弃用)', 'type' => 'switch', 'default' => false, 'help' => '公开主页对齐 Zibll 作者页为通栏布局;交易/快捷入口仅在用户中心展示。此开关保留兼容,运行时强制关闭。'],
|
||||
['key' => 'user_home_follow', 'label' => '关注按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_posts_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 10, 'min' => 5, 'max' => 50],
|
||||
['key' => 'user_home_paging_ajax', 'label' => 'AJAX 翻页', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'tabs', 'title' => 'Tab 配置', 'fields' => [
|
||||
['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits'],
|
||||
['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,favorites,comments,follow'],
|
||||
['key' => 'user_home_tabs_pc_hide', 'label' => 'PC 端隐藏', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_home_tabs_mobile_hide', 'label' => '移动端隐藏', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'author_main_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'user_home_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_desc', 'label' => '显示个人简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_stats', 'label' => '显示统计数据(已弃用)', 'type' => 'switch', 'default' => false, 'help' => '计数已并入 Tab 角标(对齐 Zibll),头图内不再重复展示文章/评论/收藏。'],
|
||||
['key' => 'user_home_social', 'label' => '显示社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_message', 'label' => '私信/消息按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_home_private', 'label' => '留言按钮', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'key' => 'other',
|
||||
'title' => '其它页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'page-header', 'title' => '统一页头', 'fields' => [
|
||||
['key' => 'page_header_style', 'label' => '页面标题样式', 'type' => 'select', 'default' => '1', 'options' => [
|
||||
['label' => '不显示', 'value' => ''],
|
||||
['label' => '简单', 'value' => '1'],
|
||||
['label' => '卡片', 'value' => '2'],
|
||||
['label' => '封面', 'value' => '3'],
|
||||
]],
|
||||
['key' => 'page_header_cover_img', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
['key' => 'page_404', 'title' => '404 页面', 'fields' => [
|
||||
['key' => 'page_404_message', 'label' => '404 提示文字', 'type' => 'input', 'default' => '页面未找到'],
|
||||
['key' => 'page_404_search', 'label' => '显示搜索框', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'page_404_popular', 'label' => '显示热门文章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'page_404_back', 'label' => '显示返回首页', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'search_results', 'title' => '搜索结果页', 'fields' => [
|
||||
['key' => 'page_search_sidebar', 'label' => '搜索结果侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'page_search_per_page', 'label' => '每页结果数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
]],
|
||||
['key' => 'author', 'title' => '作者页面', 'fields' => [
|
||||
['key' => 'page_author_cover', 'label' => '显示封面', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'page_author_sidebar', 'label' => '显示侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'page_author_posts_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'special' => [
|
||||
'key' => 'special',
|
||||
'title' => '专题页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'header', 'title' => '专题头部', 'fields' => [
|
||||
['key' => 'special_cover_enabled', 'label' => '显示专题封面', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'special_summary_enabled', 'label' => '显示专题摘要', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'special_summary_length', 'label' => '摘要最大长度', 'type' => 'number', 'default' => 200, 'min' => 50, 'max' => 500],
|
||||
['key' => 'special_default_cover', 'label' => '默认封面图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
['key' => 'layout', 'title' => '布局设置', 'fields' => [
|
||||
['key' => 'special_layout', 'label' => '内容布局', 'type' => 'radio', 'default' => 'grid', 'options' => [
|
||||
['label' => '网格', 'value' => 'grid'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
['label' => '瀑布流', 'value' => 'masonry'],
|
||||
]],
|
||||
['key' => 'special_columns', 'label' => '网格列数', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 4],
|
||||
['key' => 'special_sidebar', 'label' => '侧边栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'special_post_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'aggregation', 'title' => '聚合规则', 'fields' => [
|
||||
['key' => 'special_source_type', 'label' => '内容来源', 'type' => 'radio', 'default' => 'category', 'options' => [
|
||||
['label' => '分类', 'value' => 'category'],
|
||||
['label' => '标签', 'value' => 'tag'],
|
||||
['label' => '手动选择', 'value' => 'manual'],
|
||||
['label' => '最新文章', 'value' => 'latest'],
|
||||
]],
|
||||
['key' => 'special_source_ids', 'label' => '来源 ID 列表', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'subpage', 'title' => '子页面展示', 'fields' => [
|
||||
['key' => 'special_subpage_display', 'label' => '子页面样式', 'type' => 'radio', 'default' => 'cards', 'options' => [
|
||||
['label' => '卡片', 'value' => 'cards'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
['label' => '时间线', 'value' => 'timeline'],
|
||||
]],
|
||||
['key' => 'special_subpage_count', 'label' => '每页子页面数', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 24],
|
||||
]],
|
||||
['key' => 'pagination', 'title' => '分页设置', 'fields' => [
|
||||
['key' => 'special_pagination', 'label' => '分页模式', 'type' => 'radio', 'default' => 'numeric', 'options' => [
|
||||
['label' => '数字分页', 'value' => 'numeric'],
|
||||
['label' => '加载更多', 'value' => 'load_more'],
|
||||
['label' => '无限滚动', 'value' => 'infinite'],
|
||||
]],
|
||||
['key' => 'special_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
]],
|
||||
['key' => 'orderby', 'title' => '排序设置', 'fields' => [
|
||||
['key' => 'special_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '最近更新', 'value' => 'modified'],
|
||||
['label' => '浏览数', 'value' => 'views'],
|
||||
['label' => '评论数', 'value' => 'comments'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'background' => [
|
||||
'key' => 'background',
|
||||
'title' => '全局背景',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'light', 'title' => '亮色背景', 'fields' => [
|
||||
['key' => 'bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'bg_image', 'label' => '背景图片', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'bg_size', 'label' => '图片尺寸', 'type' => 'select', 'default' => 'cover', 'options' => [
|
||||
['label' => '覆盖', 'value' => 'cover'],
|
||||
['label' => '包含', 'value' => 'contain'],
|
||||
['label' => '原始大小', 'value' => 'auto'],
|
||||
['label' => '拉伸填充', 'value' => '100% 100%'],
|
||||
['label' => '100% 宽度(旧版兼容)', 'value' => '100%'],
|
||||
]],
|
||||
['key' => 'bg_position', 'label' => '图片位置', 'type' => 'select', 'default' => 'center center', 'options' => [
|
||||
['label' => '居中', 'value' => 'center center'],
|
||||
['label' => '顶部居中', 'value' => 'top center'],
|
||||
['label' => '底部居中', 'value' => 'bottom center'],
|
||||
]],
|
||||
['key' => 'bg_repeat', 'label' => '图片重复', 'type' => 'select', 'default' => 'no-repeat', 'options' => [
|
||||
['label' => '不重复', 'value' => 'no-repeat'],
|
||||
['label' => '平铺', 'value' => 'repeat'],
|
||||
['label' => '水平平铺', 'value' => 'repeat-x'],
|
||||
['label' => '垂直平铺', 'value' => 'repeat-y'],
|
||||
]],
|
||||
['key' => 'bg_attachment', 'label' => '固定方式', 'type' => 'select', 'default' => 'fixed', 'options' => [
|
||||
['label' => '固定', 'value' => 'fixed'],
|
||||
['label' => '随内容滚动', 'value' => 'scroll'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'dark', 'title' => '暗色背景', 'fields' => [
|
||||
['key' => 'bg_dark_color', 'label' => '暗色背景颜色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'bg_dark_image', 'label' => '暗色背景图片', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
['key' => 'overlay', 'title' => '遮罩设置', 'fields' => [
|
||||
['key' => 'bg_overlay', 'label' => '启用背景遮罩', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bg_overlay_color', 'label' => '遮罩颜色', 'type' => 'input', 'default' => 'rgba(0, 0, 0, 0.3)'],
|
||||
]],
|
||||
['key' => 'page', 'title' => '页面背景', 'fields' => [
|
||||
['key' => 'page_bg_color', 'label' => '内容区背景色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'page_bg_image', 'label' => '内容区背景图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'menu-decoration' => [
|
||||
'key' => 'menu-decoration',
|
||||
'title' => '菜单装饰',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'icon', 'title' => '菜单图标', 'fields' => [
|
||||
['key' => 'menu_icon_enabled', 'label' => '显示菜单图标', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'menu_badge_enabled', 'label' => '显示菜单徽章', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'submenu', 'title' => '下拉子菜单', 'fields' => [
|
||||
['key' => 'menu_submenu_style', 'label' => '子菜单样式', 'type' => 'radio', 'default' => 'dropdown', 'options' => [
|
||||
['label' => '普通下拉', 'value' => 'dropdown'],
|
||||
['label' => '多列下拉', 'value' => 'mega'],
|
||||
['label' => '图文卡片', 'value' => 'cards'],
|
||||
]],
|
||||
['key' => 'menu_submenu_columns', 'label' => '多列列数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 5],
|
||||
['key' => 'menu_submenu_width', 'label' => '子菜单宽度 (px)', 'type' => 'number', 'default' => 220, 'min' => 160, 'max' => 800],
|
||||
]],
|
||||
['key' => 'effect', 'title' => '交互效果', 'fields' => [
|
||||
['key' => 'menu_hover_effect', 'label' => 'Hover 效果', 'type' => 'radio', 'default' => 'underline', 'options' => [
|
||||
['label' => '下划线', 'value' => 'underline'],
|
||||
['label' => '背景色', 'value' => 'background'],
|
||||
['label' => '文字变色', 'value' => 'color'],
|
||||
['label' => '无效果', 'value' => 'none'],
|
||||
]],
|
||||
['key' => 'menu_item_padding', 'label' => '菜单项内边距 (px)', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 32],
|
||||
]],
|
||||
['key' => 'mobile', 'title' => '移动端菜单', 'fields' => [
|
||||
['key' => 'menu_mobile_style', 'label' => '移动端菜单样式', 'type' => 'radio', 'default' => 'drawer', 'options' => [
|
||||
['label' => '抽屉', 'value' => 'drawer'],
|
||||
['label' => '全屏', 'value' => 'fullscreen'],
|
||||
['label' => '下拉', 'value' => 'dropdown'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'footer' => [
|
||||
'key' => 'footer',
|
||||
'title' => '底部页脚',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'layout', 'title' => '页脚布局', 'fields' => [
|
||||
['key' => 'footer_columns', 'label' => '页脚列数', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 4],
|
||||
['key' => 'footer_copyright', 'label' => '显示版权信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_icp', 'label' => '显示ICP备案', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'icp_number', 'label' => 'ICP备案号', 'type' => 'input', 'default' => '', 'help' => '例如:ICP 备 xxxxxxxx 号'],
|
||||
['key' => 'footer_police', 'label' => '显示公安备案', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'police_number', 'label' => '公安备案号', 'type' => 'input', 'default' => '', 'help' => '例如:公网安备 xxxxxx 号'],
|
||||
]],
|
||||
['key' => 'color', 'title' => '页脚颜色', 'fields' => [
|
||||
['key' => 'footer_theme_custom', 'label' => '自定义页脚颜色', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'footer_theme_bg_custom', 'label' => '自定义背景色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'footer_theme_bg', 'label' => '背景图片', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'footer_theme_color_custom', 'label' => '自定义文字颜色', 'type' => 'color', 'default' => ''],
|
||||
]],
|
||||
['key' => 'contact', 'title' => '联系方式', 'fields' => [
|
||||
['key' => 'footer_contact_wechat_img', 'label' => '微信二维码', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'footer_contact_qq', 'label' => 'QQ', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'footer_contact_weibo', 'label' => '微博', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'footer_contact_email', 'label' => '邮箱', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'brand', 'title' => '品牌列(第一列)', 'fields' => [
|
||||
['key' => 'footer_t1_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'footer_t1_img', 'label' => '页脚 Logo(日间)', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'footer_t1_img_dark', 'label' => '页脚 Logo(夜间)', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'footer_t1_t', 'label' => '品牌标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'fcode_t1_code', 'label' => '品牌列自定义代码', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'contact_ext', 'title' => '联系方式扩展', 'fields' => [
|
||||
['key' => 'footer_contact_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'footer_contact_more', 'label' => '更多联系项', 'type' => 'textarea', 'default' => '', 'help' => 'JSON 数组,每项含 desc/icon/link/img'],
|
||||
['key' => 'fcode_t2_code_1', 'label' => '链接 HTML(fcode_t2_code_1)', 'type' => 'textarea', 'default' => '', 'help' => '直接写 <a> 链接 HTML;为空时回退快速链接/友链/热门分类'],
|
||||
['key' => 'fcode_t2_code_2', 'label' => '链接下方自定义代码', 'type' => 'textarea', 'default' => '', 'help' => '备案号等 HTML'],
|
||||
]],
|
||||
['key' => 'mini_qr', 'title' => '二维码图片组(第三列)', 'fields' => [
|
||||
['key' => 'footer_mini_img_m_s', 'label' => '显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'footer_mini_img', 'label' => '二维码组', 'type' => 'textarea', 'default' => '', 'help' => 'JSON 数组,每项含 text/img(最多4个)'],
|
||||
]],
|
||||
['key' => 'custom', 'title' => '底部自定义', 'fields' => [
|
||||
['key' => 'fcode_customize_code', 'label' => '底部自定义代码', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'tabbar' => [
|
||||
'key' => 'tabbar',
|
||||
'title' => '底部Tab栏',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => 'Tab栏设置', 'fields' => [
|
||||
['key' => 'footer_tabbar_s', 'label' => '启用底部Tab栏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_scroll_hide', 'label' => '滚动时隐藏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'footer_tabbar_scroll_down_hide', 'label' => '下滑时隐藏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_max_items', 'label' => '最大Tab数', 'type' => 'number', 'default' => 5, 'min' => 3, 'max' => 7],
|
||||
['key' => 'footer_tabbar_icon_size', 'label' => '图标大小', 'type' => 'number', 'default' => 22, 'min' => 16, 'max' => 32],
|
||||
['key' => 'footer_tabbar_text_size', 'label' => '文字大小', 'type' => 'number', 'default' => 10, 'min' => 8, 'max' => 14],
|
||||
['key' => 'footer_tabbar_show_text', 'label' => '显示文字', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_active_color', 'label' => '选中颜色', 'type' => 'color', 'default' => '#f04494'],
|
||||
['key' => 'footer_tabbar_bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'footer_tabbar_border', 'label' => '显示上边框', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_shadow', 'label' => '显示阴影', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_animation', 'label' => '切换动画', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_double_click_home', 'label' => '双击首页刷新', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'footer_tabbar_badge_dot', 'label' => '徽标圆点样式', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'footer_tabbar_single', 'label' => '文章页专属Tab', 'type' => 'select', 'default' => 'default', 'options' => [
|
||||
['label' => '默认按钮', 'value' => 'default'],
|
||||
['label' => '文章扩展按钮(评论/收藏/分享)', 'value' => 'extend'],
|
||||
], 'help' => '文章详情页底部显示的Tab按钮类型'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<array{label:string,value:string,image:string}> */
|
||||
private static function header_layout_options(): array {
|
||||
$base = defined('AKSRT_URI') ? trailingslashit(AKSRT_URI) . 'assets/images/header-layout/' : '';
|
||||
return [
|
||||
['label' => '布局 1(经典)', 'value' => '1', 'image' => $base . 'header_layout_1.png'],
|
||||
['label' => '布局 2(内联搜索)', 'value' => '2', 'image' => $base . 'header_layout_2.png'],
|
||||
['label' => '布局 3(紧凑)', 'value' => '3', 'image' => $base . 'header_layout_3.png'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<array{label:string,value:string,image:string}> */
|
||||
private static function mobile_header_layout_options(): array {
|
||||
$base = defined('AKSRT_URI') ? trailingslashit(AKSRT_URI) . 'assets/images/header-layout/' : '';
|
||||
return [
|
||||
['label' => '居中(含搜索按钮)', 'value' => 'center', 'image' => $base . 'mobile_header_layout_center.png'],
|
||||
['label' => '靠左', 'value' => 'left', 'image' => $base . 'mobile_header_layout_left.png'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPayment {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'payment',
|
||||
'title' => '支付&付费',
|
||||
'sections' => [
|
||||
'paid' => [
|
||||
'key' => 'paid',
|
||||
'title' => '内容付费 / 下载',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'strategy', 'title' => '内容付费策略', 'fields' => [
|
||||
['key' => 'payment_paid_post_types', 'label' => '受管内容类型', 'type' => 'input', 'default' => 'post'],
|
||||
['key' => 'payment_vip_free_download', 'label' => 'VIP / 角色白名单免扣下载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_exempt_roles', 'label' => '免扣角色列表', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'delivery', 'title' => '交付说明', 'fields' => [
|
||||
['key' => 'payment_paid_catalog_note', 'label' => '前台付费提示', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_manual_delivery_note', 'label' => '人工交付备注', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||
['key' => 'payment_currency_symbol', 'label' => '货币符号', 'type' => 'input', 'default' => '¥'],
|
||||
['key' => 'pay_show_user', 'label' => '显示购买用户', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
|
||||
['label' => '文章框顶部', 'value' => 'box_top'],
|
||||
['label' => '正文顶部', 'value' => 'top'],
|
||||
['label' => '正文底部', 'value' => 'bottom'],
|
||||
['label' => '文章框底部', 'value' => 'box_bottom'],
|
||||
]],
|
||||
['key' => 'pay_free_logged_show', 'label' => '免费资源需登录后下载', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'download', 'title' => '下载能力', 'fields' => [
|
||||
['key' => 'payment_download_enable', 'label' => '启用下载能力', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_down_alone_page', 'label' => '独立下载页面', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_download_remote_s', 'label' => '远程文件下载', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_download_verify_nonce', 'label' => '下载鉴权', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_download_user_log', 'label' => '用户下载记录条数', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'payment_download_post_log', 'label' => '内容下载记录条数', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'payment_download_remote_size', 'label' => '远程文件大小限制(KB)', 'type' => 'number', 'default' => 2048, 'min' => 1, 'max' => 999999],
|
||||
['key' => 'payment_download_remote_ext', 'label' => '远程文件格式限制', 'type' => 'input', 'default' => 'zip,rar,7z'],
|
||||
]],
|
||||
['key' => 'coupon', 'title' => '内容优惠码', 'fields' => [
|
||||
['key' => 'coupon_post_s', 'label' => '全站内容允许优惠码', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'coupon_desc', 'label' => '优惠码说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'coupon_post_s', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'recharge' => [
|
||||
'key' => 'recharge',
|
||||
'title' => '余额充值',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '充值开关', 'fields' => [
|
||||
['key' => 'pay_balance_s', 'label' => '启用余额功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_balance_desc', 'label' => '充值说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_auto_to_balance', 'label' => '自动到账到余额', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'packages', 'title' => '充值套餐', 'fields' => [
|
||||
['key' => 'pay_balance_product', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|入门包\n98|108|推荐包", 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'product', 'title' => '充值产品', 'fields' => [
|
||||
['key' => 'pay_balance_product_name', 'label' => '充值产品', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_price', 'label' => '充值价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_tag', 'label' => '价格标签', 'type' => 'input', 'default' => '元', 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_tag_class', 'label' => '标签样式', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_product_custom_s', 'label' => '自定义金额', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_product_custom_limit', 'label' => '自定义金额限制', 'type' => 'number', 'default' => 9999, 'min' => 1, 'max' => 99999, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'transfer', 'title' => '余额转账', 'fields' => [
|
||||
['key' => 'pay_balance_transfer_s', 'label' => '启用余额转账', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'pay_balance_transfer_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50, 'dependsOn' => ['key' => 'pay_balance_transfer_s', 'value' => true]],
|
||||
['key' => 'pay_balance_transfer_desc', 'label' => '转账说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_transfer_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'pass-charge', 'title' => '充值卡免扣', 'fields' => [
|
||||
['key' => 'pay_balance_pass_charge_s', 'label' => '启用余额卡密兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
|
||||
['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'exchange', 'title' => '积分兑换', 'fields' => [
|
||||
['key' => 'pay_balance_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'pay_balance_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'dependsOn' => ['key' => 'pay_balance_exchange_s', 'value' => true]],
|
||||
['key' => 'pay_balance_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_exchange_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'withdraw', 'title' => '余额提现', 'fields' => [
|
||||
['key' => 'pay_balance_withdraw_s', 'label' => '启用余额提现', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_balance_withdraw_lowest_money', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 99999, 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'vip' => [
|
||||
'key' => 'vip',
|
||||
'title' => 'VIP会员',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '会员等级', 'fields' => [
|
||||
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_1_name', 'label' => '一级会员名称', 'type' => 'input', 'default' => '一级会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享一级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>免费下载部分资源</li><li>会员专属身份标识</li><li>付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_2_name', 'label' => '二级会员名称', 'type' => 'input', 'default' => '二级会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享二级会员特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'default' => '<li>更高免费下载额度</li><li>更高等级身份标识</li><li>更多付费内容会员价</li>', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'packages', 'title' => '分档套餐', 'fields' => [
|
||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'points_exchange', 'title' => '积分兑换会员', 'fields' => [
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)'],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'benefit', 'title' => '下载权益', 'fields' => [
|
||||
['key' => 'pay_download_limit', 'label' => '普通用户下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'pay_download_speed', 'label' => '普通用户下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'pay_download_limit_vip_1', 'label' => '一级会员下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_download_speed_vip_1', 'label' => '一级会员下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_download_limit_vip_2', 'label' => '二级会员下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_download_speed_vip_2', 'label' => '二级会员下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_vip_pass_charge_s', 'label' => '会员卡密兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]],
|
||||
['key' => 'pay_vip_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'renew', 'title' => '续费设置', 'fields' => [
|
||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_renew', 'value' => true]],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['label' => '相同价格', 'value' => 'same'],
|
||||
['label' => '折扣价格', 'value' => 'discount'],
|
||||
['label' => '优惠立减', 'value' => 'reduce'],
|
||||
]],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '升级设置', 'fields' => [
|
||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]],
|
||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'revenue' => [
|
||||
'key' => 'revenue',
|
||||
'title' => '创作分成',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '分成策略', 'fields' => [
|
||||
['key' => 'pay_income_s', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_revenue_share_ratio', 'label' => '作者分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 1, 'max' => 100],
|
||||
['key' => 'ratio_vip_1', 'label' => 'VIP1分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'ratio_vip_2', 'label' => 'VIP2分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'payment_revenue_settlement_threshold', 'label' => '最小结算金额', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 99999],
|
||||
['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
|
||||
['key' => 'payment_revenue_points_rule', 'label' => '积分规则', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_revenue_note', 'label' => '结算备注', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'pay_income_desc_details', 'label' => '收益明细文案', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_income_desc_page', 'label' => '收益说明页', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'affiliate' => [
|
||||
'key' => 'affiliate',
|
||||
'title' => '推广返佣',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '返佣策略', 'fields' => [
|
||||
['key' => 'pay_rebate_s', 'label' => '启用推广返佣', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'pay_rebate_ratio', 'label' => '返佣比例(%)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100],
|
||||
['key' => 'payment_affiliate_window_days', 'label' => '归因窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
|
||||
['key' => 'payment_affiliate_require_paid_order', 'label' => '仅对已支付订单返佣', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_rebate_self_s', 'label' => '自购返佣', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_rebate_show_users', 'label' => '显示推广用户', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'user-ratio', 'title' => '用户返佣比例', 'fields' => [
|
||||
['key' => 'payment_affiliate_user_ratio', 'label' => '普通用户比例(%)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 100],
|
||||
['key' => 'pay_rebate_ratio_vip_1', 'label' => 'VIP1比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'pay_rebate_ratio_vip_2', 'label' => 'VIP2比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
]],
|
||||
['key' => 'poster', 'title' => '推广海报', 'fields' => [
|
||||
['key' => 'pay_rebate_poster_s', 'label' => '启用推广海报', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_rebate_poster_img', 'label' => '海报背景图', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'pay_rebate_poster_title', 'label' => '海报标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'pay_rebate_poster_desc', 'label' => '海报描述', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'text', 'title' => '文案设置', 'fields' => [
|
||||
['key' => 'pay_rebate_text_desc', 'label' => '返佣说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_rebate_text_details_title', 'label' => '明细标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'pay_rebate_text_details', 'label' => '明细说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_rebate_text_discount', 'label' => '折扣文案', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'gateway' => [
|
||||
'key' => 'gateway',
|
||||
'title' => '收款接口',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||
['key' => 'payment_gateway_notice', 'label' => '通道说明', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '以渠道开关 + 凭据是否齐全为准。异步回调由各网关验签(支付宝/微信/Stripe/易支付等),无需额外口令。卡密为兑换入口,不出现在收银台支付方式列表。'],
|
||||
]],
|
||||
['key' => 'gateways', 'title' => '支付渠道开关', 'fields' => [
|
||||
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => true, 'help' => '需同时在「余额充值」启用余额功能。关闭后用户中心仍可有钱包,但结账不可用余额支付。'],
|
||||
['key' => 'payment_alipay_enabled', 'label' => '支付宝', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_paypal_enabled', 'label' => 'PayPal', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_stripe_enabled', 'label' => 'Stripe', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_epay_enabled', 'label' => '易支付', 'type' => 'switch', 'default' => false, 'help' => '兼容彩虹易支付协议的聚合收银台,可同时开启支付宝/微信子通道。'],
|
||||
['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false, 'help' => '仅控制卡密兑换入口是否开放;卡密规则与批量发卡在「充值卡」页。不进入结账支付方式列表。'],
|
||||
]],
|
||||
// Channel credential groups: SPA uses collapsible + expandWhen + group dependsOn.
|
||||
// Field-level dependsOn kept for contract audit / non-SPA consumers.
|
||||
['key' => 'wechat', 'title' => '微信支付配置', 'collapsible' => true, 'expandWhen' => 'payment_wechat_enabled', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true], 'fields' => [
|
||||
['key' => 'wechat_merchantid', 'label' => '商户号', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
['key' => 'wechat_appid', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
['key' => 'wechat_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'native', 'options' => [
|
||||
['value' => 'native', 'label' => '扫码支付(Native)'],
|
||||
['value' => 'h5', 'label' => 'H5支付'],
|
||||
], 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
['key' => 'wechat_api_v3_key', 'label' => 'APIv3密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
['key' => 'wechat_private_key_path', 'label' => '商户私钥路径', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
['key' => 'wechat_cert_path', 'label' => '商户API证书路径', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_wechat_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'alipay', 'title' => '支付宝配置', 'collapsible' => true, 'expandWhen' => 'payment_alipay_enabled', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true], 'fields' => [
|
||||
['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'textarea', 'sanitize_type' => 'code', 'default' => '', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'page', 'options' => [
|
||||
['value' => 'page', 'label' => '电脑网站支付(web)'],
|
||||
['value' => 'wap', 'label' => '手机网站支付(wap)'],
|
||||
], 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
['key' => 'alipay_notify_help', 'label' => '回调地址说明', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '异步通知 / 同步返回已固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return},无需在此填写。', 'dependsOn' => ['key' => 'payment_alipay_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'paypal', 'title' => 'PayPal配置', 'collapsible' => true, 'expandWhen' => 'payment_paypal_enabled', 'dependsOn' => ['key' => 'payment_paypal_enabled', 'value' => true], 'fields' => [
|
||||
['key' => 'paypal_client_id', 'label' => 'Client ID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_paypal_enabled', 'value' => true]],
|
||||
['key' => 'paypal_client_secret', 'label' => 'Client Secret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_paypal_enabled', 'value' => true]],
|
||||
['key' => 'paypal_currency', 'label' => '货币', 'type' => 'input', 'default' => 'USD', 'dependsOn' => ['key' => 'payment_paypal_enabled', 'value' => true]],
|
||||
['key' => 'paypal_sandbox', 'label' => '沙箱模式', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_paypal_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'stripe', 'title' => 'Stripe配置', 'collapsible' => true, 'expandWhen' => 'payment_stripe_enabled', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true], 'fields' => [
|
||||
['key' => 'stripe_secret_key', 'label' => 'Secret Key', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
|
||||
['key' => 'stripe_publishable_key', 'label' => 'Publishable Key', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
|
||||
['key' => 'stripe_webhook_secret', 'label' => 'Webhook Secret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
|
||||
['key' => 'stripe_currency', 'label' => '货币', 'type' => 'input', 'default' => 'usd', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
|
||||
['key' => 'stripe_test_mode', 'label' => '测试模式', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'epay', 'title' => '易支付配置', 'collapsible' => true, 'expandWhen' => 'payment_epay_enabled', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'fields' => [
|
||||
['key' => 'epay_apiurl', 'label' => '接口地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'help' => '易支付站点根地址,如 https://pay.example.com/(末尾斜杠可选)。'],
|
||||
['key' => 'epay_pid', 'label' => '商户ID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
|
||||
['key' => 'epay_key', 'label' => '商户密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'help' => 'MD5 通信密钥。'],
|
||||
['key' => 'epay_channel_alipay', 'label' => '启用支付宝通道', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
|
||||
['key' => 'epay_channel_wxpay', 'label' => '启用微信通道', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
|
||||
['key' => 'epay_device', 'label' => '下单终端', 'type' => 'select', 'default' => 'auto', 'options' => [
|
||||
['value' => 'auto', 'label' => '自动(按 UA)'],
|
||||
['value' => 'pc', 'label' => '电脑端 submit'],
|
||||
['value' => 'mobile', 'label' => '移动端 mapi'],
|
||||
], 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
|
||||
['key' => 'epay_notify_help', 'label' => '回调地址说明', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '异步/同步回调固定为 /wp-json/aksrt/v1/payment/epay_alipay/{notify|return} 与 /payment/epay_wxpay/{notify|return},请在易支付后台放行对应 URL。', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'order' => [
|
||||
'key' => 'order',
|
||||
'title' => '订单设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '订单基础', 'fields' => [
|
||||
['key' => 'payment_order_prefix', 'label' => '订单前缀', 'type' => 'input', 'default' => 'AKSRT'],
|
||||
['key' => 'payment_order_expiry', 'label' => '订单有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
|
||||
['key' => 'pay_show_user', 'label' => '显示用户信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_no_logged_in', 'label' => '未登录提示', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_cookie_day', 'label' => 'Cookie有效期(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 31],
|
||||
['key' => 'pay_no_logged_remind', 'label' => '未登录提醒文案', 'type' => 'textarea', 'default' => '您当前未登录!建议登陆后购买,可保存购买订单'],
|
||||
['key' => 'pay_wait_float_s', 'label' => '订单等待浮动', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_order_max_minutes', 'label' => '订单最大支付时间(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||
['key' => 'pay_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
|
||||
['label' => '文章框顶部', 'value' => 'box_top'],
|
||||
['label' => '正文顶部', 'value' => 'top'],
|
||||
['label' => '正文底部', 'value' => 'bottom'],
|
||||
['label' => '文章框底部', 'value' => 'box_bottom'],
|
||||
]],
|
||||
['key' => 'pay_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_show_order_no', 'label' => '显示订单号', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_show_create_time', 'label' => '显示创建时间', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_show_pay_time', 'label' => '显示支付时间', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'status', 'title' => '状态设置', 'fields' => [
|
||||
['key' => 'payment_order_status_pending', 'label' => '待支付状态文案', 'type' => 'input', 'default' => '待支付'],
|
||||
['key' => 'payment_order_status_paid', 'label' => '已支付状态文案', 'type' => 'input', 'default' => '已支付'],
|
||||
['key' => 'payment_order_status_completed', 'label' => '已完成状态文案', 'type' => 'input', 'default' => '已完成'],
|
||||
['key' => 'payment_order_status_cancelled', 'label' => '已取消状态文案', 'type' => 'input', 'default' => '已取消'],
|
||||
['key' => 'payment_order_status_refunded', 'label' => '已退款状态文案', 'type' => 'input', 'default' => '已退款'],
|
||||
['key' => 'payment_order_status_refunding', 'label' => '退款中状态文案', 'type' => 'input', 'default' => '退款中'],
|
||||
['key' => 'payment_order_status_processing', 'label' => '处理中状态文案', 'type' => 'input', 'default' => '处理中'],
|
||||
]],
|
||||
['key' => 'service', 'title' => '服务设置', 'fields' => [
|
||||
['key' => 'pay_service', 'label' => '客服设置', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_service_icon', 'label' => '客服图标', 'type' => 'icon', 'default' => ''],
|
||||
['key' => 'pay_service_value', 'label' => '客服内容', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'pay_service_qq', 'label' => 'QQ客服', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'pay_service_wechat', 'label' => '微信客服', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'card-pass' => [
|
||||
'key' => 'card-pass',
|
||||
'title' => '充值卡',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'card-pass', 'title' => '充值卡', 'fields' => [
|
||||
['key' => 'card_pass_enabled', 'label' => '启用充值卡', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'card_pass_desc', 'label' => '充值卡说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_type', 'label' => '充值卡类型', 'type' => 'select', 'default' => 'balance', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true], 'options' => [
|
||||
['label' => '余额卡', 'value' => 'balance'],
|
||||
['label' => 'VIP卡', 'value' => 'vip'],
|
||||
['label' => '积分卡', 'value' => 'credits'],
|
||||
]],
|
||||
['key' => 'card_pass_prefix', 'label' => '卡号前缀', 'type' => 'input', 'default' => 'AK', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_length', 'label' => '卡号长度', 'type' => 'number', 'default' => 16, 'min' => 8, 'max' => 32, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_batch_size', 'label' => '批量生成数量', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'redeem', 'title' => '兑换设置', 'fields' => [
|
||||
['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 9999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'card_pass_redeem_page', 'label' => '兑换页面', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_success_msg', 'label' => '兑换成功提示', 'type' => 'input', 'default' => '兑换成功!', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_fail_msg', 'label' => '兑换失败提示', 'type' => 'input', 'default' => '卡号无效或已过期', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'withdrawal' => [
|
||||
'key' => 'withdrawal',
|
||||
'title' => '提现管理',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '提现设置', 'fields' => [
|
||||
['key' => 'pay_rebate_withdraw_lowest_money', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 99999],
|
||||
['key' => 'withdraw_service_charge', 'label' => '提现手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
|
||||
['key' => 'pay_rebate_withdraw_text_details', 'label' => '提现说明', 'type' => 'textarea', 'default' => '<div>可提现金额达到50元后即可发起提现</div>
|
||||
<div>申请提现需后台人工处理,一般2-3小时,请耐心等待</div>
|
||||
<div>如有其它疑问,请与客服联系</div>'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPermissions {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'permissions',
|
||||
'title' => '功能&权限',
|
||||
'sections' => [
|
||||
'login' => [
|
||||
'key' => 'login',
|
||||
'title' => '人机验证',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_captcha_config',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础开关', 'fields' => [
|
||||
['key' => 'enabled', 'label' => '启用人机验证', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'active_provider', 'label' => '当前服务商', 'type' => 'radio', 'default' => '', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '关闭', 'value' => ''],
|
||||
['label' => '腾讯云 Captcha', 'value' => 'tencent'],
|
||||
['label' => '阿里云 CAPTCHA 2.0', 'value' => 'aliyun'],
|
||||
['label' => 'Cloudflare Turnstile', 'value' => 'cloudflare'],
|
||||
['label' => '极验 GeeTest', 'value' => 'geetest'],
|
||||
]],
|
||||
['key' => 'mode', 'label' => '触发模式', 'type' => 'radio', 'default' => 'always', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '始终显示', 'value' => 'always'],
|
||||
]],
|
||||
['key' => 'enabled_scenarios', 'label' => '启用场景', 'type' => 'checkbox', 'default' => ['comment', 'login', 'register', 'bind', 'bind_phone', 'lostpassword', 'change_password', 'change_email', 'verify_email', 'download', 'signin'], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '发表评论', 'value' => 'comment'],
|
||||
['label' => '用户登录', 'value' => 'login'],
|
||||
['label' => '用户注册', 'value' => 'register'],
|
||||
['label' => '绑定第三方', 'value' => 'bind'],
|
||||
['label' => '绑定手机', 'value' => 'bind_phone'],
|
||||
['label' => '找回密码', 'value' => 'lostpassword'],
|
||||
['label' => '修改密码', 'value' => 'change_password'],
|
||||
['label' => '更改邮箱', 'value' => 'change_email'],
|
||||
['label' => '邮箱验证', 'value' => 'verify_email'],
|
||||
['label' => '资源下载', 'value' => 'download'],
|
||||
['label' => '每日签到', 'value' => 'signin'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'providers', 'title' => '服务商配置', 'fields' => [
|
||||
['key' => 'tencent', 'label' => '腾讯云配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
|
||||
['key' => 'aliyun', 'label' => '阿里云配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
|
||||
['key' => 'cloudflare', 'label' => 'Cloudflare 配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
|
||||
['key' => 'geetest', 'label' => '极验配置', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'comment' => [
|
||||
'key' => 'comment',
|
||||
'title' => '评论权限',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础权限', 'fields' => [
|
||||
['key' => 'comment_enabled', 'label' => '全局评论开关', 'type' => 'switch', 'default' => true, 'help' => '关闭后全站禁用评论功能'],
|
||||
['key' => 'comment_guest_allowed', 'label' => '允许游客评论', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_min_length', 'label' => '评论最少字数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 100],
|
||||
['key' => 'comment_max_length', 'label' => '评论最多字数', 'type' => 'number', 'default' => 1000, 'min' => 50, 'max' => 10000],
|
||||
['key' => 'comment_cool_down_seconds', 'label' => '评论冷却时间(秒)', 'type' => 'number', 'default' => 15, 'min' => 0, 'max' => 300],
|
||||
['key' => 'comment_max_links', 'label' => '最大链接数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 10],
|
||||
['key' => 'comment_max_images', 'label' => '最大图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 20],
|
||||
]],
|
||||
['key' => 'sensitive', 'title' => '敏感词', 'fields' => [
|
||||
['key' => 'comment_sensitive_words', 'label' => '敏感词列表', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'comment_sensitive_action', 'label' => '敏感词处理', 'type' => 'radio', 'default' => 'reject', 'options' => [
|
||||
['label' => '拒绝评论', 'value' => 'reject'],
|
||||
['label' => '替换为***', 'value' => 'replace'],
|
||||
['label' => '转入审核', 'value' => 'pending'],
|
||||
]],
|
||||
['key' => 'comment_sensitive_log', 'label' => '记录敏感词日志', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'anti-spam', 'title' => '反垃圾', 'fields' => [
|
||||
['key' => 'comment_link_filter', 'label' => '链接过滤', 'type' => 'switch', 'default' => true, 'help' => '评论包含链接自动进入审核'],
|
||||
['key' => 'comment_flood_protection', 'label' => '防刷评论', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_flood_interval', 'label' => '防刷间隔(秒)', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 60],
|
||||
['key' => 'comment_max_per_hour', 'label' => '每小时最大评论数', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'comment_avatar_required', 'label' => '要求设置头像', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'comment_nickname_required', 'label' => '要求填写昵称', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_email_required', 'label' => '要求填写邮箱', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'attachment' => [
|
||||
'key' => 'attachment',
|
||||
'title' => '附件权限',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'profile', 'title' => '头像与封面', 'fields' => [
|
||||
['key' => 'allow_avatar_upload', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'allow_cover_upload', 'label' => '允许用户上传封面图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'allow_background_upload', 'label' => '允许用户上传背景图', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'avatar_max_size', 'label' => '头像/封面大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
|
||||
]],
|
||||
['key' => 'content', 'title' => '内容上传', 'fields' => [
|
||||
['key' => 'allow_submit_images', 'label' => '允许投稿插入图片', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
|
||||
['key' => 'allow_comment_images', 'label' => '允许评论插入图片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
|
||||
]],
|
||||
['key' => 'resource', 'title' => '资源附件', 'fields' => [
|
||||
['key' => 'allow_resource_upload', 'label' => '允许资源附件上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'resource_max_size', 'label' => '资源附件大小上限 (MB)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'download' => [
|
||||
'key' => 'download',
|
||||
'title' => '下载权限',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '下载设置', 'fields' => [
|
||||
['key' => 'download_require_login', 'label' => '下载需要登录', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'download_require_credits', 'label' => '允许积分购买模式', 'type' => 'switch', 'default' => false, 'help' => '兼容字段;运行时以 cms_features.download_credit 为准。开启表示 pay_modo=积分 可收银台一次购买,不是按次扣费。'],
|
||||
['key' => 'download_credits_cost', 'label' => '默认积分价参考', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100, 'help' => '仅运营参考/兼容;实际价格以文章付费设置为准。'],
|
||||
['key' => 'download_daily_limit', 'label' => '每日下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
|
||||
['key' => 'download_link_expire', 'label' => '下载链接有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
|
||||
['key' => 'download_speed_limit', 'label' => '下载速度限制(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
|
||||
]],
|
||||
['key' => 'vip', 'title' => 'VIP 特权', 'fields' => [
|
||||
['key' => 'download_vip_free', 'label' => 'VIP 免费下载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'download_vip_daily_extra', 'label' => 'VIP每日额外次数', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 999],
|
||||
]],
|
||||
['key' => 'role', 'title' => '角色权限', 'fields' => [
|
||||
['key' => 'download_role_limits', 'label' => '角色下载限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|daily_limit,每行一条'],
|
||||
['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'],
|
||||
]],
|
||||
['key' => 'captcha', 'title' => '安全设置', 'fields' => [
|
||||
['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「人机验证」。'],
|
||||
['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'moderation' => [
|
||||
'key' => 'moderation',
|
||||
'title' => '内容审核',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_moderation_config',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '审核总开关', 'fields' => [
|
||||
['key' => 'enabled', 'label' => '启用审核系统', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'akismet', 'title' => 'Akismet', 'fields' => [
|
||||
['key' => 'akismet_enabled', 'label' => '启用 Akismet', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
|
||||
['key' => 'akismet_site_url', 'label' => 'Site URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'akismet_enabled', 'value' => true]],
|
||||
['key' => 'akismet_blog_lang', 'label' => '博客语言', 'type' => 'input', 'default' => 'zh-CN', 'dependsOn' => ['key' => 'akismet_enabled', 'value' => true]],
|
||||
['key' => 'akismet_api_key', 'label' => 'API Key', 'type' => 'input', 'inputType' => 'password', 'default' => '', 'dependsOn' => ['key' => 'akismet_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'ai', 'title' => 'AI 审核', 'fields' => [
|
||||
['key' => 'ai_enabled', 'label' => '启用 AI 审核', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
|
||||
['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入(百度/阿里云等)在「扩展 → API内容审核」中统一配置,此处仅控制审核范围和策略。', 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'ai_moderate_comments', 'label' => '用户评论', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'ai_moderate_background', 'label' => '用户资料背景', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'auto_approve_low_risk', 'label' => '低风险自动通过', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'auto_reject_high_risk', 'label' => '高风险自动拒绝', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'low_risk_max_score', 'label' => '低风险上限', 'type' => 'number', 'default' => 35, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
['key' => 'high_risk_min_score', 'label' => '高风险下限', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'keywords', 'title' => '关键词拦截', 'fields' => [
|
||||
['key' => 'blocked_keywords', 'label' => '屏蔽关键词列表', 'type' => 'textarea', 'default' => '', 'rows' => 4, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [
|
||||
['key' => 'rate_limit_enabled', 'label' => '启用频率限制', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
|
||||
['key' => 'rate_limit_min_interval', 'label' => '最小间隔(秒)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 300, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
|
||||
['key' => 'rate_limit_max_per_hour', 'label' => '每小时上限', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 200, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'geoip', 'title' => 'GeoIP 拦截', 'fields' => [
|
||||
['key' => 'geoip_enabled', 'label' => '启用 GeoIP 拦截', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
|
||||
['key' => 'geoip_hint', 'label' => 'IP 数据源', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '复用「扩展 → IP归属地」中配置的数据源和 API Key。', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
|
||||
['key' => 'geoip_block_countries', 'label' => '屏蔽国家/地区', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'capabilities' => [
|
||||
'key' => 'capabilities',
|
||||
'title' => '基本权限',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
// Live editing uses PermissionsCapabilities.vue + /permissions/capabilities.
|
||||
// Keep an empty schema contract here so domain build() does not fatally call
|
||||
// a missing build_cap_matrix_groups() helper when loading login/moderation.
|
||||
'groups' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPostList {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'post_list',
|
||||
'title' => '文章&列表',
|
||||
'sections' => [
|
||||
'list' => PostList\SchemaPostListList::build(),
|
||||
'thumb' => [
|
||||
'key' => 'thumb',
|
||||
'title' => '列表缩略图',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'source', 'title' => '图片来源', 'fields' => [
|
||||
['key' => 'thumb_priority', 'label' => '图片优先级', 'type' => 'input', 'default' => 'featured,attachment,resource,custom'],
|
||||
['key' => 'thumb_fallback', 'label' => '无图时处理', 'type' => 'radio', 'default' => 'placeholder', 'options' => [
|
||||
['label' => '显示占位图', 'value' => 'placeholder'],
|
||||
['label' => '隐藏缩略图', 'value' => 'hide'],
|
||||
['label' => '随机封面', 'value' => 'random'],
|
||||
]],
|
||||
['key' => 'thumb_spare_images', 'label' => '备用缩略图', 'type' => 'gallery', 'default' => [], 'dependsOn' => ['key' => 'thumb_fallback', 'value' => 'random'], 'description' => '当 thumb_fallback 为 random 时从此图库随机选取'],
|
||||
['key' => 'thumb_aspect_ratio', 'label' => '图片比例', 'type' => 'select', 'default' => '16:9', 'options' => [
|
||||
['label' => '16:9 宽屏', 'value' => '16:9'],
|
||||
['label' => '4:3 标准', 'value' => '4:3'],
|
||||
['label' => '1:1 正方形', 'value' => '1:1'],
|
||||
['label' => '自适应', 'value' => 'auto'],
|
||||
]],
|
||||
['key' => 'list_thumb_fit', 'label' => '缩略图填充方式', 'type' => 'radio', 'default' => 'cover', 'options' => [
|
||||
['label' => '填充裁剪', 'value' => 'cover'],
|
||||
['label' => '完整显示', 'value' => 'contain'],
|
||||
]],
|
||||
['key' => 'list_thumb_slides_s', 'label' => '幻灯片缩略图', 'type' => 'switch', 'default' => true, 'description' => '允许幻灯片作为列表缩略图'],
|
||||
['key' => 'list_thumb_video_s', 'label' => '视频缩略图', 'type' => 'switch', 'default' => true, 'description' => '鼠标悬停时自动播放视频缩略图'],
|
||||
['key' => 'list_thumb_video_mute_s', 'label' => '视频缩略图静音', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'list_thumb_video_s', 'value' => true], 'description' => '视频缩略图播放时静音'],
|
||||
['key' => 'thumb_postfirstimg_s', 'label' => '自动获取文章首图', 'type' => 'switch', 'default' => true, 'description' => '自动从文章内容中提取第一张图片作为缩略图'],
|
||||
['key' => 'thumb_catimg_s', 'label' => '自动获取分类封面', 'type' => 'switch', 'default' => true, 'description' => '无特色图时使用分类封面图作为缩略图'],
|
||||
['key' => 'thumb_postfirstimg_size', 'label' => '首图尺寸', 'type' => 'radio', 'default' => 'medium', 'dependsOn' => ['key' => 'thumb_postfirstimg_s', 'value' => true], 'options' => [
|
||||
['label' => '缩略图', 'value' => 'thumbnail'],
|
||||
['label' => '中等', 'value' => 'medium'],
|
||||
['label' => '大', 'value' => 'large'],
|
||||
['label' => '原始', 'value' => 'full'],
|
||||
], 'description' => '自动获取文章首图时使用的 WP 图片尺寸'],
|
||||
]],
|
||||
['key' => 'multi', 'title' => '多图模式', 'fields' => [
|
||||
['key' => 'thumb_multi_enabled', 'label' => '多图卡片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'thumb_multi_count', 'label' => '最多显示几张', 'type' => 'number', 'default' => 3, 'min' => 2, 'max' => 6, 'dependsOn' => ['key' => 'thumb_multi_enabled', 'value' => true]],
|
||||
['key' => 'mult_thumb_cat', 'label' => '按分类多图显示', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'dependsOn' => ['key' => 'thumb_multi_enabled', 'value' => true]],
|
||||
['key' => 'thumb_lazy', 'label' => '懒加载', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'thumb_multi_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'page' => [
|
||||
'key' => 'page',
|
||||
'title' => '文章页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'navigation', 'title' => '文章导航', 'fields' => [
|
||||
['key' => 'post_toc_enabled', 'label' => '目录树导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_toc_depth', 'label' => '目录深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 6],
|
||||
['key' => 'article_nav_mobile_nav_s', 'label' => '移动端弹出菜单', 'type' => 'switch', 'default' => true, 'description' => '在移动端弹出菜单中显示目录导航'],
|
||||
['key' => 'post_reading_progress', 'label' => '阅读进度条', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'breadcrumbs_single_s', 'label' => '面包屑导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'breadcrumbs_home_text', 'label' => '面包屑首页文字', 'type' => 'input', 'default' => '首页', 'depends_on' => 'breadcrumbs_single_s'],
|
||||
['key' => 'breadcrumbs_single_text', 'label' => '末项显示「正文」', 'type' => 'switch', 'default' => true, 'depends_on' => 'breadcrumbs_single_s', 'description' => '关闭后末项显示文章标题'],
|
||||
['key' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_author_card_limit', 'label' => '作者卡文章数', 'type' => 'number', 'default' => 6, 'min' => 0, 'max' => 12],
|
||||
['key' => 'post_author_card_orderby', 'label' => '作者卡文章排序', 'type' => 'select', 'default' => 'views', 'options' => [
|
||||
['label' => '浏览量', 'value' => 'views'],
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '更新时间', 'value' => 'modified'],
|
||||
['label' => '评论数', 'value' => 'comment_count'],
|
||||
]],
|
||||
['key' => 'post_author_card_style', 'label' => '作者卡文章样式', 'type' => 'radio', 'default' => 'card', 'options' => [
|
||||
['label' => '卡片', 'value' => 'card'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
]],
|
||||
['key' => 'post_single_hide_time_s', 'label' => '隐藏文章时间', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'cover', 'title' => '封面展示', 'fields' => [
|
||||
['key' => 'post_article_image_cover', 'label' => '图片封面', 'type' => 'switch', 'default' => true, 'description' => '文章有特色图时自动展示封面'],
|
||||
['key' => 'post_article_image_cover_scale', 'label' => '封面高度比例(%)', 'type' => 'number', 'default' => 35, 'min' => 10, 'max' => 100],
|
||||
['key' => 'post_article_slide_cover', 'label' => '幻灯片封面', 'type' => 'switch', 'default' => true, 'description' => '文章有图集时以轮播形式展示封面'],
|
||||
['key' => 'post_article_video_cover', 'label' => '视频封面', 'type' => 'switch', 'default' => true, 'description' => '文章有视频时以视频播放器作为封面'],
|
||||
['key' => 'post_article_video_cover_scale', 'label' => '视频封面高度比例(%)', 'type' => 'number', 'default' => 56, 'min' => 20, 'max' => 100],
|
||||
]],
|
||||
['key' => 'media', 'title' => '媒体功能', 'fields' => [
|
||||
['key' => 'post_lightbox', 'label' => '图片灯箱', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_lightbox_thumbs', 'label' => '灯箱缩略图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_lightbox_zoom', 'label' => '灯箱缩放', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_lightbox_full', 'label' => '灯箱全屏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_lightbox_down', 'label' => '灯箱下载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_lightbox_play', 'label' => '灯箱播放', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'related', 'title' => '相关推荐', 'fields' => [
|
||||
['key' => 'post_related_enabled', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_related_count', 'label' => '推荐数量', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 20],
|
||||
['key' => 'post_related_style', 'label' => '展示样式', 'type' => 'radio', 'default' => 'img', 'options' => [
|
||||
['label' => '图文卡片', 'value' => 'img'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
['label' => '纯文字', 'value' => 'text'],
|
||||
], 'description' => '对齐 Zibll 相关文章三种样式'],
|
||||
['key' => 'post_related_source', 'label' => '推荐来源', 'type' => 'radio', 'default' => 'category', 'options' => [
|
||||
['label' => '同分类/标签/专题', 'value' => 'category'],
|
||||
['label' => '同标签', 'value' => 'tag'],
|
||||
['label' => '随机', 'value' => 'random'],
|
||||
]],
|
||||
// Legacy alias kept so old saved values still round-trip.
|
||||
['key' => 'post_related_type', 'label' => '推荐方式(兼容)', 'type' => 'radio', 'default' => 'category', 'options' => [
|
||||
['label' => '同分类', 'value' => 'category'],
|
||||
['label' => '同标签', 'value' => 'tag'],
|
||||
['label' => '随机', 'value' => 'random'],
|
||||
]],
|
||||
['key' => 'post_related_orderby', 'label' => '推荐排序', 'type' => 'select', 'default' => 'views', 'options' => [
|
||||
['label' => '浏览量', 'value' => 'views'],
|
||||
['label' => '随机', 'value' => 'rand'],
|
||||
['label' => '相关度', 'value' => 'relevance'],
|
||||
['label' => '时间', 'value' => 'date'],
|
||||
['label' => '更新时间', 'value' => 'modified'],
|
||||
]],
|
||||
['key' => 'post_related_title', 'label' => '推荐标题', 'type' => 'input', 'default' => '相关推荐'],
|
||||
]],
|
||||
['key' => 'interactions', 'title' => '互动功能', 'fields' => [
|
||||
['key' => 'post_like_enabled', 'label' => '点赞', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_favorite_enabled', 'label' => '收藏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'share_s', 'label' => '分享按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_copyright_enabled', 'label' => '版权信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_copyright_text', 'label' => '版权默认文案', 'type' => 'textarea', 'default' => '', 'description' => '留空则自动生成"本文由 XX 原创发布"'],
|
||||
['key' => 'post_wechat_share', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_s', 'label' => '用户打赏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'],
|
||||
['key' => 'post_rewards_amounts', 'label' => '快捷金额', 'type' => 'input', 'default' => '5,10,20,50,100', 'description' => '英文逗号分隔,赞赏直接进入作者站内余额'],
|
||||
['key' => 'post_rewards_custom', 'label' => '允许自定义金额', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_rewards_min', 'label' => '最低金额', 'type' => 'number', 'default' => 1, 'min' => 0.01, 'max' => 1000, 'step' => 0.01],
|
||||
['key' => 'post_rewards_max', 'label' => '最高金额', 'type' => 'number', 'default' => 1000, 'min' => 1, 'max' => 100000, 'step' => 0.01],
|
||||
['key' => 'post_button_toptext', 'label' => '按钮上方文案', 'type' => 'input', 'default' => '喜欢就支持一下吧'],
|
||||
]],
|
||||
['key' => 'content', 'title' => '内容处理', 'fields' => [
|
||||
['key' => 'post_p_indent', 'label' => '段落缩进', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_maxheight_kg', 'label' => '内容高度限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_maxheight', 'label' => '最大高度', 'type' => 'number', 'default' => 1000, 'min' => 600, 'max' => 3000],
|
||||
['key' => 'post_front_content', 'label' => '文章头部内容', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'post_after_content', 'label' => '文章底部内容', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'yiyan', 'title' => '一言', 'fields' => [
|
||||
['key' => 'post_yiyan_header', 'label' => '文章头部一言', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_yiyan_footer', 'label' => '文章底部一言', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_yiyan_box', 'label' => '一言盒子', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'meta', 'title' => 'Meta设置', 'fields' => [
|
||||
['key' => 'post_default_views_min', 'label' => '默认浏览最小值', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'post_default_views_max', 'label' => '默认浏览最大值', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 9999],
|
||||
['key' => 'post_default_likes_min', 'label' => '默认点赞最小值', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'post_default_likes_max', 'label' => '默认点赞最大值', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 9999],
|
||||
['key' => 'post_views_s', 'label' => '浏览统计', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'filter' => [
|
||||
'key' => 'filter',
|
||||
'title' => '高级筛选',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '筛选基础', 'fields' => [
|
||||
['key' => 'filter_enabled', 'label' => '启用筛选栏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'custom_filter_show', 'label' => '筛选显示页面', 'type' => 'checkbox', 'default' => ['cat', 'tag'], 'options' => [
|
||||
['label' => '分类页', 'value' => 'cat'],
|
||||
['label' => '标签页', 'value' => 'tag'],
|
||||
['label' => '专题页', 'value' => 'topics'],
|
||||
], 'help' => '选择在哪些页面显示筛选栏'],
|
||||
['key' => 'filter_categories', 'label' => '分类筛选', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'filter_tags', 'label' => '标签筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'filter_sort', 'label' => '排序选项', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'filter_ajax_enabled', 'label' => 'AJAX 无刷新筛选', 'type' => 'switch', 'default' => true, 'help' => '启用后分类/标签/排序切换将通过 AJAX 加载,无需刷新页面'],
|
||||
]],
|
||||
['key' => 'ajax_dimensions', 'title' => 'AJAX 筛选维度', 'fields' => [
|
||||
['key' => 'filter_cat_show_cat', 'label' => '分类页:显示分类筛选', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
['key' => 'filter_cat_show_tag', 'label' => '分类页:显示标签筛选', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
['key' => 'filter_tag_show_cat', 'label' => '标签页:显示分类筛选', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
['key' => 'filter_tag_show_tag', 'label' => '标签页:显示标签筛选', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
['key' => 'filter_topics_show_cat', 'label' => '专题页:显示分类筛选', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
['key' => 'filter_topics_show_tag', 'label' => '专题页:显示标签筛选', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'filter_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'custom_filter', 'title' => '自定义筛选维度', 'fields' => [
|
||||
['key' => 'custom_filter', 'label' => '筛选维度定义', 'type' => 'repeater', 'default' => [], 'dependsOn' => ['key' => 'filter_enabled', 'value' => true], 'help' => '每项定义一个筛选维度(如"类型""地区""年份"),及其可选值列表', 'fields' => [
|
||||
['key' => 'name', 'label' => '维度名称', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'key', 'label' => '维度标识', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'vals', 'label' => '可选值', 'type' => 'repeater', 'default' => [], 'fields' => [
|
||||
['key' => 'key', 'label' => '标识', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'name', 'label' => '显示名', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
]],
|
||||
]],
|
||||
['key' => 'cat_custom_filter', 'title' => '按分类绑定筛选', 'fields' => [
|
||||
['key' => 'cat_custom_filter', 'label' => '分类筛选绑定', 'type' => 'repeater', 'default' => [], 'dependsOn' => ['key' => 'filter_enabled', 'value' => true], 'help' => '为指定分类绑定不同的筛选维度组合', 'fields' => [
|
||||
['key' => 'cats', 'label' => '绑定分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
|
||||
['key' => 'cat_s', 'label' => '显示分类筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'tag_s', 'label' => '显示标签筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'topics_s', 'label' => '显示专题筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'orderby_s', 'label' => '显示排序按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'custom_filters', 'label' => '启用的自定义维度', 'type' => 'input', 'default' => '', 'help' => '逗号分隔维度标识,留空使用全部'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'feature' => [
|
||||
'key' => 'feature',
|
||||
'title' => '文章功能',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'tools', 'title' => '工具功能', 'fields' => [
|
||||
['key' => 'feature_print', 'label' => '打印文章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'feature_word_count', 'label' => '字数统计', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'feature_reading_time', 'label' => '预计阅读时间', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'feature_pdf_export', 'label' => 'PDF 导出', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'comment' => [
|
||||
'key' => 'comment',
|
||||
'title' => '评论设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '评论展示', 'fields' => [
|
||||
['key' => 'comment_order', 'label' => '评论排序', 'type' => 'radio', 'default' => 'asc', 'options' => [
|
||||
['label' => '正序', 'value' => 'asc'],
|
||||
['label' => '倒序', 'value' => 'desc'],
|
||||
]],
|
||||
['key' => 'comment_per_page', 'label' => '每页评论数', 'type' => 'number', 'default' => 20, 'min' => 5, 'max' => 100],
|
||||
['key' => 'comment_pagination_type', 'label' => '分页方式', 'type' => 'radio', 'default' => 'numbered', 'options' => [
|
||||
['label' => '翻页', 'value' => 'numbered'],
|
||||
['label' => '加载更多', 'value' => 'load_more'],
|
||||
['label' => '无限滚动', 'value' => 'infinite'],
|
||||
]],
|
||||
['key' => 'comment_paging_ajax_ias_s', 'label' => '评论自动加载', 'type' => 'switch', 'default' => true, 'description' => '评论无限滚动时自动触发加载'],
|
||||
['key' => 'comment_paging_ajax_ias_max', 'label' => '评论最大加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10, 'description' => '0=不限'],
|
||||
['key' => 'comment_nested_depth', 'label' => '回复嵌套深度', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 10],
|
||||
['key' => 'comment_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 48, 'min' => 24, 'max' => 96],
|
||||
]],
|
||||
['key' => 'permissions', 'title' => '评论权限', 'fields' => [
|
||||
['key' => 'comment_require_login', 'label' => '评论需要登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'comment_allow_reply', 'label' => '允许回复', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'comment_edit_window', 'label' => '作者自助编辑时限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1440],
|
||||
]],
|
||||
['key' => 'toolbar', 'title' => '评论工具栏', 'fields' => [
|
||||
['key' => 'comment_smilie', 'label' => 'Emoji表情', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_code', 'label' => '代码插入', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_img', 'label' => '图片上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_upload_img', 'label' => '评论图片上传', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'interaction', 'title' => '互动功能', 'fields' => [
|
||||
['key' => 'comment_hot_threshold', 'label' => '神评点赞阈值', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 999],
|
||||
['key' => 'comment_like_s', 'label' => '评论点赞', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_city_s', 'label' => '评论城市', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_city_type', 'label' => '城市显示格式', 'type' => 'radio', 'default' => 'province_city', 'options' => [
|
||||
['label' => '省份+城市', 'value' => 'province_city'],
|
||||
['label' => '仅省份', 'value' => 'province'],
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'comment_quick_often', 'label' => '快捷回复短语', 'type' => 'textarea', 'default' => "不错,学习了
|
||||
感谢分享
|
||||
期待更新"],
|
||||
['key' => 'comment_author_tag', 'label' => '作者标签', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'comment_corderby', 'label' => '评论排序切换', 'type' => 'switch', 'default' => true, 'description' => '前台显示最新/最早/最热切换按钮'],
|
||||
]],
|
||||
['key' => 'text', 'title' => '文案配置', 'fields' => [
|
||||
['key' => 'comment_empty_text', 'label' => '空评论提示', 'type' => 'input', 'default' => '暂无评论,来做第一个评论的人吧~'],
|
||||
['key' => 'comment_title', 'label' => '评论区标题', 'type' => 'input', 'default' => '评论'],
|
||||
['key' => 'comment_submit_text', 'label' => '提交按钮文案', 'type' => 'input', 'default' => '提交评论'],
|
||||
['key' => 'comment_text', 'label' => '评论框提示', 'type' => 'input', 'default' => '说点什么吧...'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'pay' => [
|
||||
'key' => 'pay',
|
||||
'title' => '付费内容',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '付费设置', 'fields' => [
|
||||
['key' => 'post_pay_enable', 'label' => '启用付费内容', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_pay_price_default', 'label' => '默认价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'post_pay_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_pay_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'content', 'options' => [
|
||||
['label' => '内容前', 'value' => 'content'],
|
||||
['label' => '全文遮罩', 'value' => 'mask'],
|
||||
]],
|
||||
['key' => 'post_pay_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||
['key' => 'post_pay_show_user', 'label' => '显示购买用户', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'download', 'title' => '下载设置', 'fields' => [
|
||||
['key' => 'post_download_enable', 'label' => '启用下载功能', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_download_remote_s', 'label' => '远程文件下载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_download_verify_nonce', 'label' => '下载鉴权', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_download_user_log', 'label' => '用户下载日志', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_download_post_log', 'label' => '文章下载日志', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'share' => [
|
||||
'key' => 'share',
|
||||
'title' => '分享设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '分享设置', 'fields' => [
|
||||
['key' => 'share_wechat_s', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'share_wechat_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'share_wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'share_only_logo', 'label' => '仅显示Logo', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'share_img_compatible_s', 'label' => '海报兼容模式', 'type' => 'switch', 'default' => false, 'description' => '启用后使用兼容模式生成分享海报'],
|
||||
['key' => 'share_img_byimg', 'label' => '海报默认图', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'share_logo', 'label' => '海报LOGO', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'share_img', 'label' => '分享图片', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'share_desc', 'label' => '分享描述', 'type' => 'textarea', 'default' => '扫一扫 立即查看'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'key' => 'submit',
|
||||
'title' => '前台投稿',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础配置', 'fields' => [
|
||||
['key' => 'front_submit_enabled', 'label' => '开启前台投稿', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_roles', 'label' => '允许投稿的角色', 'type' => 'input', 'default' => 'author,editor,administrator'],
|
||||
['key' => 'post_article_btn_txte', 'label' => '投稿按钮文案', 'type' => 'input', 'default' => '发布文章'],
|
||||
['key' => 'post_article_title_strlen_limit_min', 'label' => '标题最少字数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
|
||||
['key' => 'post_article_title_strlen_limit_max', 'label' => '标题最多字数', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 200],
|
||||
['key' => 'post_t_placeholder', 'label' => '标题占位符', 'type' => 'input', 'default' => '', 'description' => '留空使用默认'],
|
||||
['key' => 'post_c_placeholder', 'label' => '内容占位符', 'type' => 'textarea', 'default' => '', 'description' => '留空使用默认'],
|
||||
]],
|
||||
['key' => 'fields', 'title' => '表单字段', 'fields' => [
|
||||
['key' => 'front_submit_title', 'label' => '标题', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_content', 'label' => '正文', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_excerpt', 'label' => '摘要', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_category', 'label' => '分类选择', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_cat', 'label' => '允许投稿分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => [], 'help' => '为空则允许全部分类'],
|
||||
['key' => 'front_submit_guest_enabled', 'label' => '允许免登录投稿', 'type' => 'switch', 'default' => false, 'help' => '开启后访客可投稿;文章归属到下方用户'],
|
||||
['key' => 'post_article_user', 'label' => '免登录归属用户', 'type' => 'user_select', 'default' => 0, 'help' => '免登录投稿时文章归属的用户,搜索选择'],
|
||||
['key' => 'front_submit_captcha', 'label' => '提交人机验证', 'type' => 'switch', 'default' => false, 'help' => '提交正式投稿时校验人机验证'],
|
||||
['key' => 'front_submit_tags', 'label' => '标签输入', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_topics_s', 'label' => '专题选择', 'type' => 'switch', 'default' => false, 'help' => '投稿时可选择专题分类'],
|
||||
['key' => 'post_article_custom_filter_s', 'label' => '高级筛选选择', 'type' => 'switch', 'default' => true, 'help' => '投稿时可选择自定义筛选维度值'],
|
||||
['key' => 'front_submit_custom_fields', 'label' => '自定义字段', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'review', 'title' => '审核策略', 'fields' => [
|
||||
['key' => 'front_submit_review_mode', 'label' => '审核模式', 'type' => 'radio', 'default' => 'pending', 'options' => [
|
||||
['label' => '待审核', 'value' => 'pending'],
|
||||
['label' => '直接发布', 'value' => 'publish'],
|
||||
['label' => '自动草稿', 'value' => 'draft'],
|
||||
]],
|
||||
['key' => 'front_submit_cover_required', 'label' => '封面图必填', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'front_submit_max_images', 'label' => '正文最大图片数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 50],
|
||||
]],
|
||||
['key' => 'editor', 'title' => '编辑器设置', 'fields' => [
|
||||
['key' => 'front_submit_editor_mode', 'label' => '编辑器模式', 'type' => 'radio', 'default' => 'rich', 'options' => [
|
||||
['label' => '富文本', 'value' => 'rich'],
|
||||
['label' => 'Markdown', 'value' => 'markdown'],
|
||||
['label' => '纯文本', 'value' => 'plain'],
|
||||
]],
|
||||
['key' => 'front_submit_draft_save', 'label' => '自动保存草稿', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'front_submit_leave_warning', 'label' => '离开页面提醒', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'paid', 'title' => '付费投稿权限', 'fields' => [
|
||||
['key' => 'post_article_pay_free_s', 'label' => '允许设置免费', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_pay_price_min', 'label' => '最低价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最低付费金额'],
|
||||
['key' => 'post_article_pay_price_max', 'label' => '最高价格', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最高付费金额'],
|
||||
['key' => 'post_article_pay_vip_price_s', 'label' => '允许设置会员价', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_pay_vip_1_discount', 'label' => 'VIP1 折扣(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'help' => 'VIP1 会员折扣百分比,80 即 8 折'],
|
||||
['key' => 'post_article_pay_vip_2_discount', 'label' => 'VIP2 折扣(%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100, 'help' => 'VIP2 会员折扣百分比,50 即 5 折'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaTools {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'tools',
|
||||
'title' => '工具&诊断',
|
||||
'sections' => [
|
||||
'import-export' => SchemaToolsImportExport::build(),
|
||||
'health' => SchemaToolsHealth::build(),
|
||||
'backup' => SchemaToolsBackup::build(),
|
||||
'email-rollback' => SchemaToolsEmailRollback::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsBackup {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'backup',
|
||||
'title' => '数据备份',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'backup', 'title' => '备份设置', 'fields' => [
|
||||
['key' => 'backup_auto', 'label' => '自动备份', 'type' => 'switch', 'help' => '启用自动定时备份'],
|
||||
['key' => 'backup_frequency', 'label' => '备份频率', 'type' => 'select', 'default' => 'weekly', 'options' => [
|
||||
['label' => '每天', 'value' => 'daily'],
|
||||
['label' => '每周', 'value' => 'weekly'],
|
||||
['label' => '每月', 'value' => 'monthly'],
|
||||
]],
|
||||
['key' => 'backup_retain', 'label' => '保留份数', 'type' => 'number', 'default' => 7, 'min' => 3, 'max' => 20],
|
||||
['key' => 'backup_now', 'label' => '立即备份', 'type' => 'button', 'help' => '手动触发一次完整备份'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsEmailRollback {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'email-rollback',
|
||||
'title' => '邮件回滚',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'rollback', 'title' => '邮件回滚', 'fields' => [
|
||||
['key' => 'rollback_type', 'label' => '回滚类型', 'type' => 'select', 'options' => [
|
||||
['label' => '按时间范围', 'value' => 'date_range'],
|
||||
['label' => '按邮件类型', 'value' => 'email_type'],
|
||||
['label' => '全部回滚', 'value' => 'all'],
|
||||
]],
|
||||
['key' => 'rollback_date_from', 'label' => '开始日期', 'type' => 'date'],
|
||||
['key' => 'rollback_date_to', 'label' => '结束日期', 'type' => 'date'],
|
||||
['key' => 'rollback_execute', 'label' => '执行回滚', 'type' => 'button', 'danger' => true, 'help' => '此操作不可逆'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsHealth {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'health',
|
||||
'title' => '健康检查',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'check', 'title' => '环境检查', 'fields' => [
|
||||
['key' => 'php_version', 'label' => 'PHP 版本', 'type' => 'display', 'help' => '建议 7.4+'],
|
||||
['key' => 'wp_version', 'label' => 'WordPress 版本', 'type' => 'display', 'help' => '建议 5.8+'],
|
||||
['key' => 'memory_limit', 'label' => '内存限制', 'type' => 'display', 'help' => '建议 256M+'],
|
||||
['key' => 'upload_max_size', 'label' => '上传限制', 'type' => 'display'],
|
||||
['key' => 'extensions', 'label' => '必要扩展', 'type' => 'display', 'help' => 'curl, mbstring, json, gd'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsImportExport {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'import-export',
|
||||
'title' => '导入导出',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'export', 'title' => '导出配置', 'fields' => [
|
||||
['key' => 'export_settings', 'label' => '导出主题设置', 'type' => 'button', 'help' => '导出当前所有主题配置为 JSON 文件'],
|
||||
['key' => 'export_options', 'label' => '导出 WordPress 选项', 'type' => 'button', 'help' => '导出 aksrt_settings 等选项'],
|
||||
]],
|
||||
['key' => 'import', 'title' => '导入配置', 'fields' => [
|
||||
['key' => 'import_settings', 'label' => '导入主题设置', 'type' => 'file', 'accept' => '.json', 'help' => '导入主题配置文件(将覆盖当前设置)'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,784 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaUsers {
|
||||
|
||||
private static function default_level_options(): array {
|
||||
$options = [];
|
||||
for ($level = 1; $level <= 10; $level++) {
|
||||
$options['name_' . $level] = 'LV' . $level;
|
||||
$options['icon_img_' . $level] = '';
|
||||
$options['upgrade_integral_' . $level] = $level === 1 ? 0 : ($level - 1) * 500 * $level;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private static function default_integral_options(): array {
|
||||
return [
|
||||
'day_max' => 100, 'sign_up' => 20, 'sign_in' => 5, 'followed' => 5,
|
||||
'post_new' => 5, 'post_like' => 1, 'post_favorite' => 2,
|
||||
'comment_new' => 2, 'comment_like' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'users',
|
||||
'title' => '用户&互动',
|
||||
'sections' => [
|
||||
'auth' => [
|
||||
'key' => 'auth',
|
||||
'title' => '注册登录',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'register', 'title' => '注册设置', 'fields' => [
|
||||
['key' => 'close_sign', 'label' => '一键禁用登录注册', 'type' => 'switch', 'default' => false, 'desc' => '开启后前台禁用全部注册/登录入口(含社交登录),不影响后台登录'],
|
||||
['key' => 'user_close_signup', 'label' => '关闭注册', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'close_signin', 'label' => '禁用账密登录', 'type' => 'switch', 'default' => false, 'desc' => '仅允许社交账号登录'],
|
||||
['key' => 'user_agreement_s', 'label' => '用户协议', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_agreement_page', 'label' => '协议页面', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_privacy_s', 'label' => '隐私政策', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_privacy_page', 'label' => '隐私页面', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_agreement_check', 'label' => '强制勾选协议', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'no_repetition_name', 'label' => '禁止重复昵称', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_nickname_out', 'label' => '昵称黑名单关键词', 'type' => 'textarea', 'default' => "赌博,博彩,代发,代理,刷单,博彩,六合彩,admin,test,管理员,客服", 'desc' => '一行一个关键词,注册昵称包含关键词将被拒绝'],
|
||||
['key' => 'user_email_limit_type', 'label' => '邮箱限制类型', 'type' => 'radio', 'default' => 'black', 'options' => [
|
||||
['label' => '黑名单(拒绝以下后缀)', 'value' => 'black'],
|
||||
['label' => '白名单(仅允许以下后缀)', 'value' => 'white'],
|
||||
]],
|
||||
['key' => 'user_email_out', 'label' => '邮箱限制后缀', 'type' => 'textarea', 'default' => "@tempmail.com\n@throwaway.email\n@guerrillamail.com", 'desc' => '一行一个邮箱后缀,如 @tempmail.com'],
|
||||
]],
|
||||
['key' => 'login', 'title' => '登录设置', 'fields' => [
|
||||
['key' => 'user_close_login', 'label' => '关闭登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_sign_type', 'label' => '登录注册样式', 'type' => 'radio', 'default' => 'page', 'options' => [
|
||||
['label' => '弹窗', 'value' => 'modal'],
|
||||
['label' => '跳转页面', 'value' => 'page'],
|
||||
]],
|
||||
['key' => 'sign_modal_auto_s', 'label' => '自动弹出登录窗', 'type' => 'switch', 'default' => false, 'desc' => '仅在 user_sign_type=modal 时生效'],
|
||||
['key' => 'sign_modal_auto_waiting_time', 'label' => '弹窗等待秒数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 60, 'desc' => '页面加载多少秒后弹出登录窗'],
|
||||
['key' => 'sign_modal_auto_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 720, 'desc' => '关闭弹窗后多少小时内不再自动弹出,0=每次刷新都弹'],
|
||||
['key' => 'replace_wp_login', 'label' => '替换WP登录页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'admin_sign_key', 'label' => '应急登录 Key', 'type' => 'input', 'default' => '', 'desc' => '留空为禁用。管理员可通过 /wp-login.php?aksrt_bypass=<key> 绕过前台替换'],
|
||||
['key' => 'user_signin_phone_s', 'label' => '手机号登录', 'type' => 'switch', 'default' => false, 'desc' => '允许用手机号作为用户名登录'],
|
||||
['key' => 'user_signin_nopas_s', 'label' => '免密登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_signin_nopas_type', 'label' => '免密登录方式', 'type' => 'radio', 'default' => 'email', 'options' => [
|
||||
['label' => '邮箱验证码', 'value' => 'email'],
|
||||
['label' => '手机验证码', 'value' => 'phone'],
|
||||
['label' => '邮箱或手机', 'value' => 'email_phone'],
|
||||
]],
|
||||
['key' => 'user_signin_nopas_active', 'label' => '免密显示模式', 'type' => 'radio', 'default' => 'nopas_first', 'options' => [
|
||||
['label' => '优先免密', 'value' => 'nopas_first'],
|
||||
['label' => '优先密码', 'value' => 'password_first'],
|
||||
['label' => '仅免密', 'value' => 'nopas_only'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'signup_captcha', 'title' => '注册验证', 'fields' => [
|
||||
['key' => 'user_signup_captch', 'label' => '注册需验证', 'type' => 'switch', 'default' => true, 'desc' => '注册时需完成邮箱或手机验证'],
|
||||
['key' => 'captch_type', 'label' => '验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
|
||||
['label' => '邮箱', 'value' => 'email'],
|
||||
['label' => '手机', 'value' => 'phone'],
|
||||
['label' => '邮箱或手机', 'value' => 'email_phone'],
|
||||
]],
|
||||
['key' => 'user_signup_no_repas', 'label' => '无需重复密码', 'type' => 'switch', 'default' => true, 'desc' => '注册时不要求二次确认密码'],
|
||||
['key' => 'user_repas_captch_type', 'label' => '找回密码验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
|
||||
['label' => '邮箱', 'value' => 'email'],
|
||||
['label' => '手机', 'value' => 'phone'],
|
||||
['label' => '邮箱或手机', 'value' => 'email_phone'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'bind', 'title' => '绑定设置', 'fields' => [
|
||||
['key' => 'bind_phone', 'label' => '绑定手机', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bind_reminder', 'label' => '绑定提醒', 'type' => 'radio', 'default' => 'close', 'options' => [
|
||||
['label' => '提醒绑定邮箱', 'value' => 'email'],
|
||||
['label' => '提醒绑定手机', 'value' => 'phone'],
|
||||
['label' => '提醒绑定邮箱和手机', 'value' => 'email_phone'],
|
||||
['label' => '关闭', 'value' => 'close'],
|
||||
]],
|
||||
['key' => 'bind_reminder_text', 'label' => '提醒文案', 'type' => 'input', 'default' => '请绑定手机号以正常使用'],
|
||||
['key' => 'bind_reminder_expires', 'label' => '提醒周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 720, 'desc' => '用户关闭提醒后多少小时内不再提醒'],
|
||||
['key' => 'mandatory_bind', 'label' => '强制绑定', 'type' => 'radio', 'default' => 'close', 'options' => [
|
||||
['label' => '强制绑定邮箱', 'value' => 'email'],
|
||||
['label' => '强制绑定手机', 'value' => 'phone'],
|
||||
['label' => '强制绑定邮箱和手机', 'value' => 'email_phone'],
|
||||
['label' => '关闭', 'value' => 'close'],
|
||||
]],
|
||||
['key' => 'email_set_captch', 'label' => '绑定邮箱需验证', 'type' => 'switch', 'default' => true, 'desc' => '绑定邮箱时需发送验证码确认'],
|
||||
]],
|
||||
['key' => 'invite_inline', 'title' => '邀请码注册', 'fields' => [
|
||||
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false, 'desc' => '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段'],
|
||||
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
|
||||
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
|
||||
['label' => '必须邀请码', 'value' => 'must'],
|
||||
]],
|
||||
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
|
||||
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
|
||||
]],
|
||||
['key' => 'sign_page_ui', 'title' => '页面 UI 配置', 'fields' => [
|
||||
['key' => 'sign_page_background', 'label' => '页面背景图', 'type' => 'gallery', 'default' => [], 'desc' => '登录注册页背景图,多图随机'],
|
||||
['key' => 'sign_theme_mode', 'label' => '主题模式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '亮色', 'value' => 'light'],
|
||||
['label' => '暗色', 'value' => 'dark'],
|
||||
]],
|
||||
['key' => 'sign_show_header', 'label' => '显示顶部导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sign_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'center', 'options' => [
|
||||
['label' => '靠左', 'value' => 'left'],
|
||||
['label' => '居中', 'value' => 'center'],
|
||||
['label' => '靠右', 'value' => 'right'],
|
||||
]],
|
||||
['key' => 'sign_show_logo', 'label' => '显示 LOGO', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sign_page_footer', 'label' => '页脚 HTML', 'type' => 'textarea', 'default' => '', 'desc' => '登录注册页自定义页脚内容,支持 HTML'],
|
||||
]],
|
||||
['key' => 'modal_ui', 'title' => '弹窗 UI 配置', 'fields' => [
|
||||
['key' => 'sign_modal_background', 'label' => '弹窗背景图', 'type' => 'gallery', 'default' => [], 'desc' => '弹窗左侧背景图,多图随机'],
|
||||
['key' => 'sign_modal_show_logo', 'label' => '弹窗显示 LOGO', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'sign_logo', 'title' => '登录框 LOGO', 'fields' => [
|
||||
['key' => 'user_logo', 'label' => '日间 LOGO', 'type' => 'gallery', 'default' => [], 'desc' => '登录框专用日间 LOGO,留空使用站点全局 LOGO'],
|
||||
['key' => 'user_logo_dark', 'label' => '夜间 LOGO', 'type' => 'gallery', 'default' => [], 'desc' => '登录框专用夜间 LOGO,留空使用站点全局夜间 LOGO'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'notifications' => [
|
||||
'key' => 'notifications',
|
||||
'title' => '消息通知',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础设置', 'fields' => [
|
||||
['key' => 'message_s', 'label' => '站内通知总开关', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'msg_center_rewrite_slug', 'label' => '消息中心 URL 别名', 'type' => 'input', 'default' => 'message'],
|
||||
['key' => 'message_icon_show', 'label' => '通知图标位置', 'type' => 'checkbox', 'default' => ['nav_menu'], 'options' => [
|
||||
['label' => '导航菜单', 'value' => 'nav_menu'],
|
||||
['label' => '用户头像', 'value' => 'avatar'],
|
||||
['label' => '移动端底部', 'value' => 'mobile_bar'],
|
||||
]],
|
||||
['key' => 'message_paginate_type', 'label' => '列表翻页模式', 'type' => 'radio', 'default' => 'ajax_lists', 'options' => [
|
||||
['label' => 'AJAX 翻页', 'value' => 'ajax_lists'],
|
||||
['label' => '传统翻页', 'value' => 'default'],
|
||||
]],
|
||||
['key' => 'message_user_set', 'label' => '允许用户设置推送', 'type' => 'switch', 'default' => true, 'desc' => '允许用户在前台自行设置通知推送偏好'],
|
||||
]],
|
||||
['key' => 'types', 'title' => '通知类型', 'fields' => [
|
||||
['key' => 'message_close_msg_type', 'label' => '关闭部分推送', 'type' => 'checkbox', 'default' => [], 'options' => [
|
||||
['label' => '文章通知', 'value' => 'posts'],
|
||||
['label' => '评论回复', 'value' => 'comment'],
|
||||
['label' => '收藏通知', 'value' => 'favorite'],
|
||||
['label' => '点赞通知', 'value' => 'like'],
|
||||
['label' => '热门通知', 'value' => 'hot'],
|
||||
['label' => '关注通知', 'value' => 'followed'],
|
||||
['label' => '系统通知', 'value' => 'system'],
|
||||
['label' => '支付通知', 'value' => 'pay'],
|
||||
['label' => '提现回复', 'value' => 'withdraw_reply'],
|
||||
['label' => '勋章通知', 'value' => 'medal'],
|
||||
['label' => '认证回复', 'value' => 'auth_apply_reply'],
|
||||
], 'desc' => '勾选的类型将不向用户推送通知'],
|
||||
]],
|
||||
['key' => 'auto_clear', 'title' => '自动清理', 'fields' => [
|
||||
['key' => 'message_auto_clear_s', 'label' => '自动清理已读消息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'message_auto_clear_time', 'label' => '过期天数', 'type' => 'number', 'default' => 365, 'min' => 1, 'max' => 3650, 'desc' => '已读消息超过此天数将被自动清理'],
|
||||
['key' => 'message_auto_clear_types', 'label' => '清理类型', 'type' => 'checkbox', 'default' => ['posts', 'comment', 'favorite', 'like', 'hot', 'followed', 'system'], 'options' => [
|
||||
['label' => '文章通知', 'value' => 'posts'],
|
||||
['label' => '评论回复', 'value' => 'comment'],
|
||||
['label' => '收藏通知', 'value' => 'favorite'],
|
||||
['label' => '点赞通知', 'value' => 'like'],
|
||||
['label' => '热门通知', 'value' => 'hot'],
|
||||
['label' => '关注通知', 'value' => 'followed'],
|
||||
['label' => '系统通知', 'value' => 'system'],
|
||||
['label' => '支付通知', 'value' => 'pay'],
|
||||
['label' => '提现回复', 'value' => 'withdraw_reply'],
|
||||
['label' => '勋章通知', 'value' => 'medal'],
|
||||
['label' => '认证回复', 'value' => 'auth_apply_reply'],
|
||||
['label' => '下载通知', 'value' => 'download'],
|
||||
['label' => '积分变动', 'value' => 'credits'],
|
||||
['label' => '私信', 'value' => 'private'],
|
||||
['label' => '举报反馈', 'value' => 'report'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'private', 'title' => '私信设置', 'fields' => [
|
||||
['key' => 'private_s', 'label' => '启用私信功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'private_smilie_s', 'label' => '支持表情', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'private_code_s', 'label' => '支持代码', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'private_image_s', 'label' => '支持图片', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'private_upload_img', 'label' => '支持上传图片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'private_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'private_quick_often', 'label' => '常用快捷回复', 'type' => 'textarea', 'default' => "你好,请问有什么可以帮你的?\n感谢你的反馈\n已处理,请查收", 'desc' => '一行一条快捷回复文案'],
|
||||
['key' => 'private_submit_text', 'label' => '发送按钮文案', 'type' => 'input', 'default' => '发送'],
|
||||
['key' => 'private_placeholder', 'label' => '输入占位符', 'type' => 'input', 'default' => '请输入消息...'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'oauth' => [
|
||||
'key' => 'oauth',
|
||||
'title' => '第三方登录',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '基础设置', 'fields' => [
|
||||
['key' => 'oauth_s', 'label' => '启用第三方登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_proxy_mode', 'label' => '代理登录模式', 'type' => 'radio', 'default' => 'close', 'options' => [
|
||||
['label' => '关闭', 'value' => 'close'],
|
||||
['label' => '服务端', 'value' => 'server'],
|
||||
['label' => '客户端', 'value' => 'client'],
|
||||
]],
|
||||
['key' => 'oauth_proxy_secret', 'label' => '代理 HMAC 密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'oauth_proxy_mode', 'not' => 'close']],
|
||||
['key' => 'oauth_proxy_server_url', 'label' => '服务端地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'client']],
|
||||
['key' => 'oauth_proxy_server_providers', 'label' => '服务端允许平台', 'type' => 'checkbox', 'default' => ['github', 'gitee', 'qq', 'google'], 'options' => [
|
||||
['label' => 'GitHub', 'value' => 'github'],
|
||||
['label' => 'Gitee', 'value' => 'gitee'],
|
||||
['label' => 'QQ', 'value' => 'qq'],
|
||||
['label' => 'Google', 'value' => 'google'],
|
||||
['label' => '微博', 'value' => 'weibo'],
|
||||
['label' => '微信', 'value' => 'weixin'],
|
||||
['label' => '百度', 'value' => 'baidu'],
|
||||
['label' => '支付宝', 'value' => 'alipay'],
|
||||
], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'server']],
|
||||
['key' => 'oauth_proxy_client_providers', 'label' => '客户端启用平台', 'type' => 'checkbox', 'default' => ['github', 'gitee', 'qq', 'google'], 'options' => [
|
||||
['label' => 'GitHub', 'value' => 'github'],
|
||||
['label' => 'Gitee', 'value' => 'gitee'],
|
||||
['label' => 'QQ', 'value' => 'qq'],
|
||||
['label' => 'Google', 'value' => 'google'],
|
||||
['label' => '微博', 'value' => 'weibo'],
|
||||
['label' => '微信', 'value' => 'weixin'],
|
||||
['label' => '百度', 'value' => 'baidu'],
|
||||
['label' => '支付宝', 'value' => 'alipay'],
|
||||
], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'client']],
|
||||
['key' => 'oauth_proxy_gzh_type', 'label' => '代理公众号类型', 'type' => 'radio', 'default' => 'service', 'options' => [
|
||||
['label' => '服务号', 'value' => 'service'],
|
||||
['label' => '订阅号', 'value' => 'subscribe'],
|
||||
], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'not' => 'close']],
|
||||
['key' => 'oauth_bind_type', 'label' => '绑定方式', 'type' => 'radio', 'default' => 'auto', 'options' => [
|
||||
['label' => '自动绑定', 'value' => 'auto'],
|
||||
['label' => '手动绑定', 'value' => 'manual'],
|
||||
]],
|
||||
['key' => 'oauth_button_lg', 'label' => '大按钮样式', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'weixingzh_priority', 'label' => 'PC端优先微信扫码', 'type' => 'switch', 'default' => false, 'help' => '开启后 PC 端登录框优先显示微信公众号扫码'],
|
||||
['key' => 'weixingzh_auto', 'label' => '微信APP内自动登录', 'type' => 'switch', 'default' => false, 'help' => '在微信内置浏览器中自动使用公众号授权登录'],
|
||||
['key' => 'weixingzh_auto_expires', 'label' => '自动登录频率限制 (秒)', 'type' => 'number', 'default' => 3600, 'min' => 0, 'max' => 86400, 'dependsOn' => ['key' => 'weixingzh_auto', 'value' => true]],
|
||||
]],
|
||||
['key' => 'providers', 'title' => '登录提供商', 'fields' => [
|
||||
['key' => 'oauth_github_s', 'label' => 'GitHub登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_gitee_s', 'label' => 'Gitee登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_qq_s', 'label' => 'QQ登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_weixingzh_s', 'label' => '微信公众号登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_weixin_s', 'label' => '微信开放平台登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_weibo_s', 'label' => '微博登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_baidu_s', 'label' => '百度登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_alipay_s', 'label' => '支付宝登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'clogin_s', 'label' => '彩虹聚合登录', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'github', 'title' => 'GitHub配置', 'fields' => [
|
||||
['key' => 'github_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'github_appkey', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'gitee', 'title' => 'Gitee配置', 'fields' => [
|
||||
['key' => 'gitee_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'gitee_appkey', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'qq', 'title' => 'QQ配置', 'fields' => [
|
||||
['key' => 'qq_appid', 'label' => 'App ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'qq_appkey', 'label' => 'App Key', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'weixin', 'title' => '微信开放平台配置', 'fields' => [
|
||||
['key' => 'weixin_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixin_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'weixingzh', 'title' => '微信公众号配置', 'fields' => [
|
||||
['key' => 'weixingzh_gzh_type', 'label' => '公众号类型', 'type' => 'radio', 'default' => 'service', 'options' => [
|
||||
['label' => '服务号', 'value' => 'service'],
|
||||
['label' => '订阅号', 'value' => 'subscribe'],
|
||||
]],
|
||||
['key' => 'weixingzh_token', 'label' => 'Token', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixingzh_token_nocheck', 'label' => '不验证Token', 'type' => 'switch', 'default' => false, 'help' => '开发调试时可跳过 Token 验证'],
|
||||
['key' => 'weixingzh_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixingzh_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixingzh_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixingzh_code_qrcode', 'label' => '公众号二维码', 'type' => 'media', 'default' => '', 'help' => '引导用户扫码关注登录的二维码图片'],
|
||||
['key' => 'weixingzh_code_keyword', 'label' => '登录回复关键词', 'type' => 'input', 'default' => '登录', 'help' => '用户在公众号内回复此关键词触发登录验证码'],
|
||||
['key' => 'weixingzh_code_reply', 'label' => '验证码回复模板', 'type' => 'textarea', 'default' => '您的验证码是:%s,有效期 %d 分钟。', 'help' => '%s=验证码,%d=有效分钟数'],
|
||||
['key' => 'weixingzh_code_subscribe_reply', 'label' => '新关注回复模板', 'type' => 'textarea', 'default' => '欢迎关注!回复「%s」获取登录验证码。', 'help' => '%s=登录关键词'],
|
||||
['key' => 'weixingzh_subscribe_msg', 'label' => '关注后回复', 'type' => 'textarea', 'default' => '', 'help' => '用户关注公众号后的自动回复文案'],
|
||||
['key' => 'weixingzh_new_subscribe_msg', 'label' => '新用户关注回复', 'type' => 'textarea', 'default' => '', 'help' => '首次关注用户的专属回复文案,留空使用通用回复'],
|
||||
['key' => 'weixingzh_scan_msg', 'label' => '扫码回复', 'type' => 'textarea', 'default' => '', 'help' => '用户扫码关注公众号后的回复文案'],
|
||||
['key' => 'weixingzh_auto_reply_text', 'label' => '文本自动回复', 'type' => 'textarea', 'default' => '', 'help' => '收到文本消息的自动回复'],
|
||||
['key' => 'weixingzh_auto_reply_search', 'label' => '搜索回复', 'type' => 'textarea', 'default' => '', 'help' => '未匹配关键词时的搜索提示回复'],
|
||||
['key' => 'weixingzh_auto_reply_image', 'label' => '图片回复', 'type' => 'textarea', 'default' => '', 'help' => '收到图片消息的自动回复'],
|
||||
['key' => 'weixingzh_auto_reply_voice', 'label' => '语音回复', 'type' => 'textarea', 'default' => '', 'help' => '收到语音消息的自动回复'],
|
||||
['key' => 'weixingzh_auto_reply_default', 'label' => '默认回复', 'type' => 'textarea', 'default' => '暂不支持此类型的消息', 'help' => '其他未匹配消息类型的兜底回复'],
|
||||
['key' => 'weixingzh_gzh_menu', 'label' => '自定义菜单 JSON', 'type' => 'textarea', 'default' => '', 'help' => '微信公众号自定义菜单 JSON 配置,留空不创建菜单'],
|
||||
]],
|
||||
['key' => 'weibo', 'title' => '微博配置', 'fields' => [
|
||||
['key' => 'weibo_appid', 'label' => 'App Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weibo_appkey', 'label' => 'App Secret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'baidu', 'title' => '百度配置', 'fields' => [
|
||||
['key' => 'baidu_appid', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'baidu_appkey', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'clogin', 'title' => '彩虹聚合登录', 'fields' => [
|
||||
['key' => 'clogin_url', 'label' => '接口地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true], 'help' => '如 https://xxx.com'],
|
||||
['key' => 'clogin_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
|
||||
['key' => 'clogin_key', 'label' => 'AppKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
|
||||
['key' => 'clogin_oauth_s', 'label' => '启用平台', 'type' => 'checkbox', 'default' => ['qq', 'weixin', 'weibo', 'alipay'], 'options' => [
|
||||
['label' => 'QQ', 'value' => 'qq'],
|
||||
['label' => '微信', 'value' => 'weixin'],
|
||||
['label' => '微博', 'value' => 'weibo'],
|
||||
['label' => '支付宝', 'value' => 'alipay'],
|
||||
['label' => '百度', 'value' => 'baidu'],
|
||||
], 'dependsOn' => ['key' => 'clogin_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'alipay', 'title' => '支付宝配置', 'fields' => [
|
||||
['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'textarea', 'sanitize_type' => 'code', 'default' => ''],
|
||||
['key' => 'alipay_publickey', 'label' => '支付宝公钥', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'sms' => [
|
||||
'key' => 'sms',
|
||||
'title' => '短信接口',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'provider', 'title' => '供应商配置', 'fields' => [
|
||||
['key' => 'sms_enabled', 'label' => '启用短信', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'sms_provider', 'label' => '短信服务商', 'type' => 'select', 'default' => 'aliyun', 'dependsOn' => ['key' => 'sms_enabled', 'value' => true], 'options' => [
|
||||
['label' => '阿里云短信', 'value' => 'aliyun'],
|
||||
['label' => '腾讯云短信', 'value' => 'tencent'],
|
||||
['label' => '华为云短信', 'value' => 'huawei'],
|
||||
['label' => '自定义接口', 'value' => 'custom'],
|
||||
]],
|
||||
['key' => 'sms_number_limit_ch', 'label' => '仅发国内', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true], 'desc' => '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信'],
|
||||
['key' => 'sms_access_key', 'label' => 'AccessKey / SecretId / AppKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_access_secret', 'label' => 'AccessSecret / SecretKey / AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'aliyun', 'title' => '阿里云配置', 'fields' => [
|
||||
['key' => 'sms_ali_api_type', 'label' => '接口类型', 'type' => 'radio', 'default' => 'sms', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'aliyun'], 'options' => [
|
||||
['label' => '短信服务', 'value' => 'sms'],
|
||||
['label' => '号码认证', 'value' => 'verify'],
|
||||
]],
|
||||
['key' => 'sms_region_id', 'label' => 'RegionId', 'type' => 'input', 'default' => 'cn-hangzhou', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'aliyun']],
|
||||
]],
|
||||
['key' => 'tencent', 'title' => '腾讯云配置', 'fields' => [
|
||||
['key' => 'sms_tencent_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'tencent']],
|
||||
['key' => 'sms_tencent_app_key', 'label' => 'AppKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'tencent']],
|
||||
['key' => 'sms_tencent_secret_id', 'label' => 'SecretId', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'tencent']],
|
||||
['key' => 'sms_tencent_secret_key', 'label' => 'SecretKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'tencent']],
|
||||
['key' => 'sms_tencent_region', 'label' => '地域', 'type' => 'input', 'default' => 'ap-guangzhou', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'tencent']],
|
||||
]],
|
||||
['key' => 'huawei', 'title' => '华为云配置', 'fields' => [
|
||||
['key' => 'sms_huawei_sender', 'label' => '通道号 (sender)', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'huawei']],
|
||||
['key' => 'sms_huawei_signature', 'label' => '短信签名', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'huawei']],
|
||||
['key' => 'sms_huawei_endpoint', 'label' => 'API Endpoint', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'huawei']],
|
||||
]],
|
||||
['key' => 'custom', 'title' => '自定义接口配置', 'fields' => [
|
||||
['key' => 'sms_custom_url', 'label' => '请求 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
['key' => 'sms_custom_method', 'label' => '请求方法', 'type' => 'select', 'default' => 'POST', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom'], 'options' => [
|
||||
['label' => 'POST', 'value' => 'POST'],
|
||||
['label' => 'GET', 'value' => 'GET'],
|
||||
['label' => 'PUT', 'value' => 'PUT'],
|
||||
]],
|
||||
['key' => 'sms_custom_auth_type', 'label' => '鉴权方式', 'type' => 'select', 'default' => 'header', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom'], 'options' => [
|
||||
['label' => 'Header', 'value' => 'header'],
|
||||
]],
|
||||
['key' => 'sms_custom_auth_key', 'label' => '鉴权 Header 名', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
['key' => 'sms_custom_auth_value', 'label' => '鉴权 Header 值', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
['key' => 'sms_custom_headers', 'label' => '自定义 Headers', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
['key' => 'sms_custom_body', 'label' => '请求 Body', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
['key' => 'sms_custom_success_rule', 'label' => '成功判定规则', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_provider', 'value' => 'custom']],
|
||||
]],
|
||||
['key' => 'template', 'title' => '短信模板', 'fields' => [
|
||||
['key' => 'sms_sign_name', 'label' => '短信签名', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_template_code', 'label' => '验证码模板ID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_code_length', 'label' => '验证码长度', 'type' => 'number', 'default' => 6, 'min' => 4, 'max' => 8, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_code_expire', 'label' => '验证码有效期(秒)', 'type' => 'number', 'default' => 300, 'min' => 60, 'max' => 600, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'rate_limit', 'title' => '限流策略', 'fields' => [
|
||||
['key' => 'sms_rate_limit', 'label' => '发送间隔(秒)', 'type' => 'number', 'default' => 60, 'min' => 10, 'max' => 300, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_daily_limit', 'label' => '每日发送上限', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'triggers', 'title' => '通知场景', 'fields' => [
|
||||
['key' => 'sms_notify_bind', 'label' => '绑定手机验证', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_notify_login', 'label' => '短信登录', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_notify_register', 'label' => '注册验证', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
['key' => 'sms_notify_reset_password', 'label' => '找回密码', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'sms_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'signin' => [
|
||||
'key' => 'signin',
|
||||
'title' => '签到奖励',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'rules', 'title' => '签到规则', 'fields' => [
|
||||
['key' => 'signin_enabled', 'label' => '启用签到', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_reward', 'label' => '每日签到奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'signin_consecutive_bonus', 'label' => '连续签到加成', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_consecutive_days', 'label' => '加成周期', 'type' => 'number', 'default' => 7, 'min' => 2, 'max' => 30],
|
||||
['key' => 'signin_bonus_reward', 'label' => '周期奖励', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
|
||||
]],
|
||||
['key' => 'display', 'title' => '签到展示', 'fields' => [
|
||||
['key' => 'signin_header_user_show', 'label' => '导航栏显示签到', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_modal_auto_s', 'label' => '自动弹出签到', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'signin_modal_auto_expires', 'label' => '弹出间隔(天)', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 30],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'credits' => [
|
||||
'key' => 'credits',
|
||||
'title' => '积分设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '积分基础', 'fields' => [
|
||||
['key' => 'points_s', 'label' => '启用积分系统', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'points_pay_s', 'label' => '积分支付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'points_rate', 'label' => '积分兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000],
|
||||
['key' => 'points_name', 'label' => '积分名称', 'type' => 'input', 'default' => '积分'],
|
||||
['key' => 'points_decimal', 'label' => '积分小数位数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 4],
|
||||
]],
|
||||
['key' => 'earn', 'title' => '积分获取 / 任务', 'fields' => [
|
||||
['key' => 'points_earn_daily_limit', 'label' => '每日免费积分上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999, 'help' => '0为不限制;免费任务合计'],
|
||||
['key' => 'points_free_sign_up', 'label' => '注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 10000],
|
||||
['key' => 'points_free_sign_in', 'label' => '签到展示分值', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 1000],
|
||||
['key' => 'points_free_login', 'label' => '每日登录', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_free_post_new', 'label' => '发布文章', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000],
|
||||
['key' => 'points_free_post_like', 'label' => '文章被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_free_post_favorite', 'label' => '文章被收藏', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'],
|
||||
]],
|
||||
['key' => 'spend', 'title' => '积分消费', 'fields' => [
|
||||
['key' => 'points_spend_comment', 'label' => '评论消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_spend_download', 'label' => '下载消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_spend_view_hidden', 'label' => '查看隐藏内容消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
]],
|
||||
['key' => 'transfer', 'title' => '积分转账', 'fields' => [
|
||||
['key' => 'points_transfer_s', 'label' => '启用积分转账', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'points_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
|
||||
['key' => 'points_transfer_min', 'label' => '最低转账金额', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
|
||||
]],
|
||||
['key' => 'deduction', 'title' => '积分抵扣(预留)', 'fields' => [
|
||||
['key' => 'points_deduction_s', 'label' => '启用积分抵扣', 'type' => 'switch', 'default' => false, 'help' => '对齐 zibll:运行时暂未启用。字段保留供后续接收银台现金单抵扣。'],
|
||||
['key' => 'points_deduction_rate', 'label' => '积分换算比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '多少积分抵 1 元(zibll points_deduction_rate)。当前未参与下单。'],
|
||||
['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100, 'help' => '预留:最多可抵订单金额百分比。当前未参与下单。'],
|
||||
]],
|
||||
['key' => 'card_pass', 'title' => '积分卡密兑换', 'fields' => [
|
||||
['key' => 'points_pass_exchange_s', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'points_pass_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true]],
|
||||
['key' => 'points_pass_exchange_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'points_show_balance', 'label' => '导航栏显示余额', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'points_show_log', 'label' => '显示积分记录', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'points_expire_days', 'label' => '积分有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'invite' => [
|
||||
'key' => 'invite',
|
||||
'title' => '邀请码',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '邀请设置', 'fields' => [
|
||||
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
|
||||
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
|
||||
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
|
||||
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
|
||||
['label' => '必须邀请码', 'value' => 'must'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'format', 'title' => '码面设置', 'fields' => [
|
||||
['key' => 'invit_code_length', 'label' => '邀请码长度', 'type' => 'number', 'default' => 8, 'min' => 4, 'max' => 32],
|
||||
['key' => 'invit_code_prefix', 'label' => '邀请码前缀', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'invit_code_max_use', 'label' => '单码最大使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
|
||||
['key' => 'invit_code_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
|
||||
['key' => 'invit_code_batch', 'label' => '批量生成数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 500],
|
||||
]],
|
||||
['key' => 'reward', 'title' => '邀请奖励', 'fields' => [
|
||||
['key' => 'invit_reward_points', 'label' => '邀请奖励积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 1000],
|
||||
['key' => 'invit_reward_level', 'label' => '邀请奖励等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 10],
|
||||
['key' => 'invit_reward_vip_days', 'label' => '奖励VIP天数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 3650],
|
||||
['key' => 'invit_invitee_reward', 'label' => '被邀请人奖励', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'invit_code_show_count', 'label' => '显示已使用次数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'invit_code_show_remain', 'label' => '显示剩余次数', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'features' => [
|
||||
'key' => 'features',
|
||||
'title' => '用户功能',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。level=我的等级;auth=官方认证;等级、VIP、订单和认证会按功能开关自动注入'],
|
||||
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
['key' => 'user_city_s', 'label' => '显示城市(IP 归属地)', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_city_type', 'label' => '位置精度', 'type' => 'radio', 'default' => 'province', 'options' => [
|
||||
['label' => '省份', 'value' => 'province'],
|
||||
['label' => '省份+城市', 'value' => 'province_city'],
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
||||
['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
|
||||
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_message_s', 'label' => '启用私信功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_online_s', 'label' => '显示在线状态', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_last_active_s', 'label' => '显示最后活跃', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'profile', 'title' => '资料页字段', 'fields' => [
|
||||
['key' => 'user_profile_nickname', 'label' => '昵称', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_email', 'label' => '邮箱', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_url', 'label' => '个人主页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_desc', 'label' => '个人简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_avatar', 'label' => '头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_qq', 'label' => 'QQ', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_wechat', 'label' => '微信', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_weibo', 'label' => '微博', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_github', 'label' => 'GitHub', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_bilibili', 'label' => 'B站', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_twitter', 'label' => 'Twitter', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_linkedin', 'label' => 'LinkedIn', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_facebook', 'label' => 'Facebook', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_telegram', 'label' => 'Telegram', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_douyin', 'label' => '抖音', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_zhihu', 'label' => '知乎', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'card', 'title' => '用户卡片', 'fields' => [
|
||||
['key' => 'user_card_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_desc', 'label' => '显示简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_count', 'label' => '显示统计', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_follow', 'label' => '显示关注按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'right', 'options' => [
|
||||
['label' => '右侧', 'value' => 'right'],
|
||||
['label' => '左侧', 'value' => 'left'],
|
||||
['label' => '顶部', 'value' => 'top'],
|
||||
]],
|
||||
['key' => 'user_card_style', 'label' => '卡片样式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '简洁', 'value' => 'simple'],
|
||||
['label' => '详细', 'value' => 'detail'],
|
||||
]],
|
||||
['key' => 'user_card_bg', 'label' => '卡片背景', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_card_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 80, 'min' => 40, 'max' => 200],
|
||||
]],
|
||||
['key' => 'privacy', 'title' => '隐私设置', 'fields' => [
|
||||
['key' => 'user_privacy_posts', 'label' => '隐藏文章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_ip', 'label' => '隐藏IP归属地', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_location', 'label' => '隐藏位置', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
|
||||
],
|
||||
],
|
||||
'verify' => [
|
||||
'key' => 'verify',
|
||||
'title' => '身份认证',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '公开展示', 'fields' => [
|
||||
['key' => 'user_verify_enabled', 'label' => '启用身份认证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_personal_badge_text', 'label' => '个人认证徽标文案', 'type' => 'input', 'default' => '个人认证'],
|
||||
['key' => 'user_verify_enterprise_badge_text', 'label' => '企业认证徽标文案', 'type' => 'input', 'default' => '企业认证'],
|
||||
]],
|
||||
['key' => 'workflow', 'title' => '认证方式', 'fields' => [
|
||||
['type' => 'alert', 'content' => '个人认证使用人脸实人认证;企业认证需先通过企业要素与法人关系核验,再由法定代表人完成人脸核身。云端失败可按设置转人工审核。'],
|
||||
['key' => 'user_verify_provider', 'label' => '当前认证服务商', 'type' => 'select', 'default' => 'aliyun', 'options' => [
|
||||
['label' => '阿里云', 'value' => 'aliyun'],
|
||||
['label' => '腾讯云', 'value' => 'tencent'],
|
||||
]],
|
||||
['key' => 'user_verify_allow_manual_fallback', 'label' => '云认证失败后允许转人工', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_session_ttl_minutes', 'label' => '云认证会话有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
|
||||
['key' => 'user_verify_poll_interval_seconds', 'label' => '结果轮询间隔(秒)', 'type' => 'number', 'default' => 5, 'min' => 3, 'max' => 60],
|
||||
['key' => 'user_verify_max_retries', 'label' => '技术故障允许重试次数', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 3],
|
||||
]],
|
||||
['key' => 'aliyun', 'title' => '阿里云流程参数', 'fields' => [
|
||||
['key' => 'user_verify_aliyun_region', 'label' => 'Region', 'type' => 'input', 'default' => 'cn-hangzhou'],
|
||||
['key' => 'user_verify_aliyun_personal_flow', 'label' => '个人认证 SceneId', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_verify_aliyun_product_code', 'label' => '个人认证 ProductCode', 'type' => 'select', 'default' => 'ID_PRO', 'options' => [
|
||||
['label' => '实人认证(ID_PRO)', 'value' => 'ID_PRO'],
|
||||
['label' => '活体人脸验证(PV_FV)', 'value' => 'PV_FV'],
|
||||
['label' => '活体检测(LR_FR)', 'value' => 'LR_FR'],
|
||||
]],
|
||||
['key' => 'user_verify_aliyun_enterprise_flow', 'label' => '企业认证场景/业务标识', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'tencent', 'title' => '腾讯云流程参数', 'fields' => [
|
||||
['key' => 'user_verify_tencent_region', 'label' => 'Region', 'type' => 'input', 'default' => 'ap-guangzhou'],
|
||||
['key' => 'user_verify_tencent_personal_flow', 'label' => '个人认证 RuleId/流程标识', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_verify_tencent_enterprise_flow', 'label' => '企业认证流程标识', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'apply', 'title' => '申请设置', 'fields' => [
|
||||
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
|
||||
['key' => 'user_verify_personal_fee', 'label' => '个人认证每次扣余额(元)', 'type' => 'number', 'sanitize_type' => 'float', 'default' => 0, 'min' => 0, 'step' => 0.01],
|
||||
['key' => 'user_verify_personal_free_applications', 'label' => '个人认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。'],
|
||||
['key' => 'user_verify_enterprise_fee', 'label' => '企业认证每次扣余额(元)', 'type' => 'number', 'sanitize_type' => 'float', 'default' => 0, 'min' => 0, 'step' => 0.01],
|
||||
['key' => 'user_verify_enterprise_free_applications', 'label' => '企业认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。'],
|
||||
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'level' => [
|
||||
'key' => 'level',
|
||||
'title' => '用户等级',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
||||
['key' => 'user_level_s', 'label' => '启用用户等级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 30],
|
||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [
|
||||
['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_level_options(), 'help' => 'name_N / icon_img_N / upgrade_integral_N'],
|
||||
]],
|
||||
['key' => 'integral', 'title' => '经验值得分', 'fields' => [
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_integral_options(), 'help' => 'day_max + 行为得分键'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'badge' => [
|
||||
'key' => 'badge',
|
||||
'title' => '用户徽章',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '徽章展示', 'fields' => [
|
||||
['key' => 'user_medal_s', 'label' => '启用徽章/勋章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_medal_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_medal_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
]],
|
||||
['key' => 'catalog', 'title' => '徽章明细', 'fields' => [
|
||||
[
|
||||
'key' => 'user_medal_args',
|
||||
'label' => '分类与条目',
|
||||
'type' => 'json',
|
||||
'default' => [
|
||||
[
|
||||
'cat_name' => '成长',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'rookie',
|
||||
'name' => '初出茅庐',
|
||||
'desc' => '注册时间超过30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'registration_time',
|
||||
'get_val' => 30,
|
||||
],
|
||||
[
|
||||
'slug' => 'checkin-30',
|
||||
'name' => '签到达人',
|
||||
'desc' => '累计签到30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'checkin_all_day',
|
||||
'get_val' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'cat_name' => '限定',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'annual-star',
|
||||
'name' => '年度人气达人',
|
||||
'desc' => '活动限定徽章',
|
||||
'icon' => '',
|
||||
'get_type' => 'manually_add',
|
||||
'get_val' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'help' => '结构化目录:分类 → 条目(slug/name/desc/icon/get_type/get_val)。由 SPA UsersBadge 编辑。',
|
||||
],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'report' => [
|
||||
'key' => 'report',
|
||||
'title' => '禁封/举报',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'entry', 'title' => '前台入口', 'fields' => [
|
||||
['key' => 'user_report_enabled', 'label' => '启用举报/禁封结构', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_report_public_entry', 'label' => '公开主页展示举报入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
['key' => 'user_report_public_notice', 'label' => '公开显示限制提示', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
['key' => 'user_report_ban_notice_text', 'label' => '限制提示文案', 'type' => 'input', 'default' => '当前账号处于限制状态,部分互动能力已关闭。', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'taxonomy', 'title' => '举报分类', 'fields' => [
|
||||
['key' => 'user_report_categories', 'label' => '举报分类', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉"],
|
||||
]],
|
||||
['key' => 'ban', 'title' => '禁封设置', 'fields' => [
|
||||
['key' => 'ban_preset_reason', 'label' => '预设封禁原因', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作", 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
['key' => 'ban_appeal_desc', 'label' => '申诉说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
['key' => 'ban_appeal_keys', 'label' => '申诉关键词', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'image', 'title' => '图片举报', 'fields' => [
|
||||
['key' => 'user_report_img_s', 'label' => '启用图片举报', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
['key' => 'user_report_desc', 'label' => '举报说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'wechat' => [
|
||||
'key' => 'wechat',
|
||||
'title' => '微信通知',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'account', 'title' => '公众号配置', 'fields' => [
|
||||
['key' => 'wechat_notify_enabled', 'label' => '启用微信通知', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'wechat_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'verify', 'title' => '服务器验证', 'fields' => [
|
||||
['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'templates', 'title' => '模板消息', 'fields' => [
|
||||
['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
|
||||
['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'wechat_notify_enabled', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaProvider {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, mixed>>
|
||||
*/
|
||||
private $domain_schemas = [];
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function normalize_domain(string $domain): string {
|
||||
$domain = trim(str_replace('-', '_', $domain));
|
||||
return $domain !== '' ? $domain : 'global';
|
||||
}
|
||||
|
||||
public function normalize_section(string $section): string {
|
||||
return trim($section);
|
||||
}
|
||||
|
||||
public function get_domain_keys(): array {
|
||||
return [
|
||||
'global',
|
||||
'page_display',
|
||||
'post_list',
|
||||
'permissions',
|
||||
'users',
|
||||
'payment',
|
||||
'extensions',
|
||||
'tools',
|
||||
];
|
||||
}
|
||||
|
||||
public function get_schema(string $domain): ?array {
|
||||
$domain = $this->normalize_domain($domain);
|
||||
|
||||
if (isset($this->domain_schemas[$domain])) {
|
||||
return $this->domain_schemas[$domain];
|
||||
}
|
||||
|
||||
$builder_map = $this->get_schema_builder_map();
|
||||
if (!isset($builder_map[$domain])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$builder = $builder_map[$domain];
|
||||
$this->domain_schemas[$domain] = $builder::build();
|
||||
|
||||
return $this->domain_schemas[$domain];
|
||||
}
|
||||
|
||||
public function get_sections(string $domain): array {
|
||||
$schema = $this->get_schema($domain);
|
||||
return is_array($schema['sections'] ?? null) ? $schema['sections'] : [];
|
||||
}
|
||||
|
||||
public function get_section_schema(string $domain, string $section): ?array {
|
||||
$sections = $this->get_sections($domain);
|
||||
$section = $this->normalize_section($section);
|
||||
return $sections[$section] ?? null;
|
||||
}
|
||||
|
||||
public function get_option_key(string $domain, string $section = ''): string {
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
return (string) ($schema['option_key'] ?? 'aksrt_settings');
|
||||
}
|
||||
|
||||
$primary = [
|
||||
'global' => 'aksrt_settings',
|
||||
'page_display' => 'aksrt_settings',
|
||||
'post_list' => 'aksrt_settings',
|
||||
'permissions' => 'aksrt_settings',
|
||||
'users' => 'aksrt_settings',
|
||||
'payment' => 'aksrt_settings',
|
||||
'extensions' => 'aksrt_settings',
|
||||
'tools' => 'aksrt_settings',
|
||||
];
|
||||
|
||||
return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings';
|
||||
}
|
||||
|
||||
public function get_defaults(string $domain, string $section = ''): array {
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
return $schema ? $this->collect_defaults($schema) : [];
|
||||
}
|
||||
|
||||
$defaults = [];
|
||||
foreach ($this->get_sections($domain) as $section_schema) {
|
||||
$defaults = array_merge($defaults, $this->collect_defaults($section_schema));
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public function get_allowed_fields(string $domain, string $section = ''): array {
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
return $schema ? $this->collect_allowed_fields($schema) : [];
|
||||
}
|
||||
|
||||
$allowed = [];
|
||||
foreach ($this->get_sections($domain) as $section_schema) {
|
||||
$allowed = array_merge($allowed, $this->collect_allowed_fields($section_schema));
|
||||
}
|
||||
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
public function validate(string $domain, array $data, string $section = ''): array {
|
||||
$schemas = [];
|
||||
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
if (!$schema) {
|
||||
return ['valid' => false, 'errors' => ['Unknown section']];
|
||||
}
|
||||
$schemas[] = $schema;
|
||||
} else {
|
||||
$schemas = array_values($this->get_sections($domain));
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($schemas as $schema) {
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
$key = $field['key'];
|
||||
if (!array_key_exists($key, $data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $data[$key];
|
||||
if (!empty($field['required']) && ($value === '' || $value === null)) {
|
||||
$errors[] = sprintf('%s is required', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $field['type'] ?? 'input';
|
||||
if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
|
||||
$errors[] = sprintf('%s must be a number', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'checkbox' && !is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'repeater' && !is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
|
||||
$errors[] = sprintf('%s must be at least %s', $field['label'] ?? $key, $field['min']);
|
||||
}
|
||||
|
||||
if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
|
||||
$errors[] = sprintf('%s must be at most %s', $field['label'] ?? $key, $field['max']);
|
||||
}
|
||||
|
||||
if (in_array($type, ['select', 'radio'], true) && !empty($field['options'])) {
|
||||
$valid_values = array_column($field['options'], 'value');
|
||||
// Multi-select: accept array values when schema.multiple is set
|
||||
// (legacy / SPA may still use type=select multiple).
|
||||
if (!empty($field['multiple']) && is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if (!in_array($item, $valid_values, true)) {
|
||||
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (!in_array($value, $valid_values, true)) {
|
||||
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'checkbox' && !empty($field['options'])) {
|
||||
$valid_values = array_column($field['options'], 'value');
|
||||
if (!is_array($value)) {
|
||||
$errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
foreach ((array) $value as $item) {
|
||||
if (!in_array($item, $valid_values, true)) {
|
||||
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['valid' => empty($errors), 'errors' => $errors];
|
||||
}
|
||||
|
||||
public function read_settings(string $domain, string $section = ''): array {
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
if (!$schema) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->read_section_settings($schema);
|
||||
}
|
||||
|
||||
$settings = [];
|
||||
foreach ($this->get_sections($domain) as $section_schema) {
|
||||
$settings = array_merge($settings, $this->read_section_settings($section_schema));
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function write_settings(string $domain, array $data, string $section = ''): bool {
|
||||
if ($section !== '') {
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
if (!$schema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->write_section_settings($schema, $data);
|
||||
}
|
||||
|
||||
$result = true;
|
||||
foreach ($this->get_sections($domain) as $section_schema) {
|
||||
$allowed = array_keys($this->collect_allowed_fields($section_schema));
|
||||
$payload = array_intersect_key($data, array_flip($allowed));
|
||||
if ($payload === []) {
|
||||
continue;
|
||||
}
|
||||
$result = $this->write_section_settings($section_schema, $payload) && $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function reset_to_defaults(string $domain, string $section = ''): bool {
|
||||
if ($section !== '') {
|
||||
return $this->write_settings($domain, $this->get_defaults($domain, $section), $section);
|
||||
}
|
||||
|
||||
return $this->write_settings($domain, $this->get_defaults($domain));
|
||||
}
|
||||
|
||||
private function get_schema_builder_map(): array {
|
||||
// Builders live under AKSRT\Admin\Schema (Composer PSR-4). Relative
|
||||
// Schema\*::class resolves correctly from this AKSRT\Admin namespace.
|
||||
return [
|
||||
'global' => Schema\GlobalSchema::class,
|
||||
'page_display' => Schema\SchemaPageDisplay::class,
|
||||
'post_list' => Schema\SchemaPostList::class,
|
||||
'permissions' => Schema\SchemaPermissions::class,
|
||||
'users' => Schema\SchemaUsers::class,
|
||||
'payment' => Schema\SchemaPayment::class,
|
||||
'extensions' => Schema\SchemaExtensions::class,
|
||||
'tools' => Schema\SchemaTools::class,
|
||||
];
|
||||
}
|
||||
|
||||
private function read_section_settings(array $schema): array {
|
||||
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
|
||||
$stored = get_option($option_key, []);
|
||||
if (!is_array($stored)) {
|
||||
$stored = [];
|
||||
}
|
||||
|
||||
$data = $this->collect_defaults($schema);
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
if (array_key_exists($field['key'], $stored)) {
|
||||
$data[$field['key']] = $stored[$field['key']];
|
||||
}
|
||||
}
|
||||
|
||||
// Captcha: always surface normalized enabled_scenarios (legacy disabled list inverted).
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
|
||||
$normalized = aksrt_get_captcha_config();
|
||||
foreach (['enabled', 'active_provider', 'enabled_scenarios', 'disabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest', 'mode'] as $key) {
|
||||
if (array_key_exists($key, $normalized)) {
|
||||
$data[$key] = $normalized[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function write_section_settings(array $schema, array $data): bool {
|
||||
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
|
||||
$stored = get_option($option_key, []);
|
||||
if (!is_array($stored)) {
|
||||
$stored = [];
|
||||
}
|
||||
|
||||
$allowed = $this->collect_allowed_fields($schema);
|
||||
foreach ($allowed as $key => $type) {
|
||||
if (!array_key_exists($key, $data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->sanitize_value($data[$key], $type);
|
||||
$stored[$key] = $value;
|
||||
}
|
||||
|
||||
// Captcha: keep enabled_scenarios / disabled_scenarios mirrors in sync.
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_scenes')) {
|
||||
$all = array_keys(aksrt_captcha_scenes());
|
||||
if (array_key_exists('enabled_scenarios', $stored)) {
|
||||
$enabled = function_exists('aksrt_captcha_sanitize_scene_list')
|
||||
? aksrt_captcha_sanitize_scene_list($stored['enabled_scenarios'])
|
||||
: (array) $stored['enabled_scenarios'];
|
||||
$stored['enabled_scenarios'] = $enabled;
|
||||
$stored['disabled_scenarios'] = array_values(array_diff($all, $enabled));
|
||||
} elseif (array_key_exists('disabled_scenarios', $stored)) {
|
||||
$disabled = function_exists('aksrt_captcha_sanitize_scene_list')
|
||||
? aksrt_captcha_sanitize_scene_list($stored['disabled_scenarios'])
|
||||
: (array) $stored['disabled_scenarios'];
|
||||
$stored['disabled_scenarios'] = $disabled;
|
||||
$stored['enabled_scenarios'] = array_values(array_diff($all, $disabled));
|
||||
}
|
||||
}
|
||||
|
||||
$updated = update_option($option_key, $stored);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
if (isset($stored['site_favicon'])) {
|
||||
\AKSRT\Settings\Settings::sync_native_site_icon((string) $stored['site_favicon']);
|
||||
}
|
||||
|
||||
return $updated || true;
|
||||
}
|
||||
|
||||
private function collect_defaults(array $schema): array {
|
||||
$defaults = [];
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
if (array_key_exists('default', $field)) {
|
||||
$defaults[$field['key']] = $field['default'];
|
||||
}
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
private function collect_allowed_fields(array $schema): array {
|
||||
$allowed = [];
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
$allowed[$field['key']] = $field['sanitize_type'] ?? $this->infer_sanitize_type($field['type'] ?? 'input');
|
||||
}
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
private function get_all_fields(array $schema): array {
|
||||
$fields = [];
|
||||
foreach ($schema['groups'] ?? [] as $group) {
|
||||
if (!is_array($group)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($group['fields'] ?? [] as $field) {
|
||||
if (!is_array($field) || empty($field['key'])) {
|
||||
continue;
|
||||
}
|
||||
// Notice/help-only rows are UI chrome, not stored settings keys.
|
||||
if (($field['type'] ?? '') === 'notice') {
|
||||
continue;
|
||||
}
|
||||
$fields[] = $field;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function sanitize_value($value, string $type) {
|
||||
switch ($type) {
|
||||
case 'html':
|
||||
return wp_kses_post((string) $value);
|
||||
case 'textarea':
|
||||
return sanitize_textarea_field((string) $value);
|
||||
case 'code':
|
||||
return is_string($value) ? wp_unslash($value) : '';
|
||||
case 'url':
|
||||
return esc_url_raw((string) $value);
|
||||
case 'int':
|
||||
return (int) $value;
|
||||
case 'bool':
|
||||
return !empty($value);
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'array':
|
||||
// 深层字符串净化,保留嵌套服务商配置(如 geetest.captcha_id)
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$key = is_string($k) ? sanitize_key($k) : $k;
|
||||
if (is_array($v)) {
|
||||
$out[$key] = $this->sanitize_value($v, 'array');
|
||||
} elseif (is_bool($v)) {
|
||||
$out[$key] = $v;
|
||||
} elseif (is_int($v) || is_float($v)) {
|
||||
$out[$key] = $v;
|
||||
} else {
|
||||
$out[$key] = sanitize_text_field((string) $v);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
default:
|
||||
return sanitize_text_field((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
private function infer_sanitize_type(string $field_type): string {
|
||||
$map = [
|
||||
'switch' => 'bool',
|
||||
'input' => 'string',
|
||||
'text' => 'string',
|
||||
'textarea' => 'textarea',
|
||||
'number' => 'int',
|
||||
'slider' => 'int',
|
||||
'select' => 'string',
|
||||
'radio' => 'string',
|
||||
'checkbox' => 'array',
|
||||
'repeater' => 'array',
|
||||
'json' => 'array',
|
||||
// 自定义面板(验证码服务商等)按对象存储
|
||||
'custom' => 'array',
|
||||
'color' => 'string',
|
||||
'icon' => 'string',
|
||||
'media' => 'url',
|
||||
'code' => 'code',
|
||||
];
|
||||
|
||||
return $map[$field_type] ?? 'string';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ToolsController extends ControllerBase {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Health: email verification status
|
||||
register_rest_route(self::API_NAMESPACE, '/health/email-verification', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_health_email_verification'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Health: REST block toggle
|
||||
register_rest_route(self::API_NAMESPACE, '/health/rest-block', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_set_rest_block'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Email rollback: list users with recent email changes
|
||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_email_rollback_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Email rollback: execute rollback
|
||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users/(?P<id>\d+)/rollback', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_email_rollback_execute'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: system health (compact format for ExtensionTools.vue)
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/health', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_tools_health'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: maintenance actions
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/maintenance', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'run_maintenance'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: update center
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/update-info', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_update_info'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/update-check', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'detect_update'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/update-dismiss', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'dismiss_update'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tools/health — compact health data for the tools page.
|
||||
*/
|
||||
public function get_tools_health(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$upload_dir = wp_upload_dir();
|
||||
$upload_ok = !empty($upload_dir['error']) === false && is_dir($upload_dir['basedir']);
|
||||
|
||||
$db_ok = true;
|
||||
$db_status = '正常';
|
||||
$expected_tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_invite',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
];
|
||||
$missing = [];
|
||||
foreach ($expected_tables as $table) {
|
||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
|
||||
$missing[] = str_replace($wpdb->prefix, '', $table);
|
||||
}
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
$db_ok = false;
|
||||
$db_status = '缺少: ' . implode(', ', $missing);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'php_version' => PHP_VERSION,
|
||||
'wp_version' => get_bloginfo('version'),
|
||||
'db_status' => $db_status,
|
||||
'db_ok' => $db_ok,
|
||||
'upload_dir' => $upload_dir['basedir'],
|
||||
'upload_ok' => $upload_ok,
|
||||
'theme_version' => wp_get_theme()->get('Version') ?? 'unknown',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /tools/maintenance — run a maintenance action.
|
||||
*/
|
||||
public function run_maintenance(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
||||
|
||||
switch ($action) {
|
||||
case 'flush_cache':
|
||||
// Flush WordPress object cache
|
||||
wp_cache_flush();
|
||||
// Clear transients
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_aksrt_%' OR option_name LIKE '_transient_timeout_aksrt_%'");
|
||||
return $this->success(['message' => __('缓存已清除', 'aksrt')]);
|
||||
|
||||
case 'optimize_db':
|
||||
global $wpdb;
|
||||
$tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_invite',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
];
|
||||
$optimized = 0;
|
||||
foreach ($tables as $table) {
|
||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) {
|
||||
$wpdb->query("OPTIMIZE TABLE {$table}");
|
||||
$optimized++;
|
||||
}
|
||||
}
|
||||
return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]);
|
||||
|
||||
default:
|
||||
return $this->error('未知的维护操作: ' . esc_html($action));
|
||||
}
|
||||
}
|
||||
|
||||
public function get_update_info(): \WP_REST_Response {
|
||||
$theme = wp_get_theme();
|
||||
$info = [
|
||||
'current_version' => $theme->get('Version') ?: '0.0.0',
|
||||
'name' => $theme->get('Name') ?: 'AKSRT',
|
||||
'update_available'=> false,
|
||||
'remote_version' => '',
|
||||
'download_url' => '',
|
||||
'changelog' => '',
|
||||
'last_check' => 0,
|
||||
'dismissed' => '',
|
||||
];
|
||||
|
||||
if (class_exists('\AKSRT\Core\UpdateChecker')) {
|
||||
$stored = \AKSRT\Core\UpdateChecker::get_instance()->get_update_info();
|
||||
if (is_array($stored)) {
|
||||
$info = array_merge($info, $stored);
|
||||
}
|
||||
}
|
||||
|
||||
$info['dismissed'] = (string) get_user_meta(get_current_user_id(), 'aksrt_dismissed_update', true);
|
||||
|
||||
return $this->success(['data' => $info]);
|
||||
}
|
||||
|
||||
public function detect_update(): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Core\UpdateChecker')) {
|
||||
return $this->error(__('更新检查器不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
\AKSRT\Core\UpdateChecker::get_instance()->do_check();
|
||||
$stored = \AKSRT\Core\UpdateChecker::get_instance()->get_update_info();
|
||||
|
||||
return $this->success([
|
||||
'message' => __('已完成更新检测', 'aksrt'),
|
||||
'data' => is_array($stored) ? $stored : [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function dismiss_update(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$version = sanitize_text_field((string) ($params['version'] ?? ''));
|
||||
|
||||
if ($version === '') {
|
||||
return $this->error(__('缺少版本号', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
update_user_meta(get_current_user_id(), 'aksrt_dismissed_update', $version);
|
||||
|
||||
return $this->success([
|
||||
'message' => __('已忽略此版本提醒', 'aksrt'),
|
||||
'data' => ['version' => $version],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_health_email_verification(): \WP_REST_Response {
|
||||
$feature_enabled = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::registration_requires_email_verification();
|
||||
$pending_users = 0;
|
||||
|
||||
if (class_exists('\AKSRT\Auth\EmailVerification')) {
|
||||
global $wpdb;
|
||||
$pending_users = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_email_verified' AND CAST(meta_value AS UNSIGNED) = 0"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success(['data' => [
|
||||
'feature_enabled' => $feature_enabled,
|
||||
'pending_users' => $pending_users,
|
||||
'rest_blocking_active' => !empty(get_option('aksrt_rest_block_email_verification', false)),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function rest_set_rest_block(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$enabled = !empty($params['enabled']);
|
||||
update_option('aksrt_rest_block_email_verification', $enabled);
|
||||
|
||||
return $this->success([
|
||||
'message' => $enabled ? __('已启用 REST 邮箱验证阻断', 'aksrt') : __('已关闭 REST 邮箱验证阻断', 'aksrt'),
|
||||
'data' => ['enabled' => $enabled],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_email_rollback_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(100, max(1, (int) $request->get_param('per_page')));
|
||||
$data = \AKSRT\Auth\EmailRollback::list_pending($page, $per_page);
|
||||
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_email_rollback_execute(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
||||
return $this->error(__('邮箱回滚功能不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('id');
|
||||
$result = \AKSRT\Auth\EmailRollback::rollback($user_id, get_current_user_id());
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['error'] ?? __('邮箱回滚失败', 'aksrt')), 422);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => (string) ($result['message'] ?? __('邮箱已回滚', 'aksrt')),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class UserController extends ControllerBase {
|
||||
use UserControllerReportOperations;
|
||||
use UserControllerInviteOperations;
|
||||
use UserControllerVerifyIntegrations;
|
||||
use UserControllerIdentitySettings;
|
||||
use UserControllerAuthPhoneOperations;
|
||||
use UserControllerRestrictionOperations;
|
||||
use UserControllerFollowOperations;
|
||||
use UserControllerProfileMediaOperations;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Auth settings (login/registration)
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/auth', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_auth_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// OAuth settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/oauth', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_oauth'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// OAuth proxy settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/oauth-proxy', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_oauth_proxy'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// User profile media uploads (logged-in users).
|
||||
register_rest_route(self::API_NAMESPACE, '/user/background', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_background'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/avatar', [
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
[
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_user_avatar'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
],
|
||||
]);
|
||||
|
||||
// Comment inline image upload (logged-in users)
|
||||
register_rest_route(self::API_NAMESPACE, '/comment/upload-image', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_comment_image'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Notification settings
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/notifications', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_notification_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Keep message-center aliases on this controller so existing admin pages
|
||||
// remain compatible while notifications runtime lives in Message controller.
|
||||
register_rest_route(self::API_NAMESPACE, '/notifications/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_notification_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/notifications/mark-read', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_mark_notification_read'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/notifications/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'rest_delete_notification_item'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/notifications/cleanup', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_run_notification_cleanup'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Mail templates (email content templates)
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/mail-templates', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'handle_mail_templates'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits settings (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/settings', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_credit_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits — current user balance (frontend)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/me', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_credits_me'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Credits — user list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/users', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_list_credit_users'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits — adjust user balance (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/users/(?P<id>\d+)/adjust', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_adjust_user_credits'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits — transaction log (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/log', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_credits_log'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Report ticket list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/report/list', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_report_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/users/reports', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_report_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/users/reports/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_report_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/users/reports/(?P<id>\d+)/process', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'process_report'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Report status update (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/report/update-status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_report_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Report counts (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/report/counts', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_report_counts'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Report submit (logged-in users, front-end)
|
||||
register_rest_route(self::API_NAMESPACE, '/report/submit', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'submit_report'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Follow toggle (logged-in users)
|
||||
register_rest_route(self::API_NAMESPACE, '/follow/toggle', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'follow_toggle'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Follow status check (logged-in users)
|
||||
register_rest_route(self::API_NAMESPACE, '/follow/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'follow_status'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Follow counts (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/follow/counts', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'follow_counts'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Invite codes management (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_invite_codes'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'generate_invite_codes'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/invite/codes/(?P<id>\d+)/disable', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'disable_invite_code'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/invite/codes/bulk', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'bulk_update_invite_codes'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/invite/stats', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_invite_stats'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Modal Auth (弹窗登录/注册) ──
|
||||
|
||||
// Modal login
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/modal-login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'modal_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Modal register
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/modal-register', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'modal_register'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Current user info (for AJAX refresh)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/me', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_current_user_info'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// Identity verification provider settings (admin only)
|
||||
register_rest_route(self::API_NAMESPACE, '/identity/settings', [
|
||||
[
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_identity_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_identity_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/identity/settings/test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_identity_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// SMS provider status
|
||||
register_rest_route(self::API_NAMESPACE, '/sms/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_sms_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// SMS test send (admin only)
|
||||
register_rest_route(self::API_NAMESPACE, '/sms/test', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'test_sms_send'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// WeChat notification status
|
||||
register_rest_route(self::API_NAMESPACE, '/wechat/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_wechat_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// WeChat settings update
|
||||
register_rest_route(self::API_NAMESPACE, '/wechat/settings', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_wechat_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// SMS auth: send verification code (public, rate-limited)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/send-sms-code', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_send_sms_code'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: register by phone (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-register', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_register'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: login by phone + password (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: login by phone + SMS code (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-code-login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_code_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: bind phone to current user (logged-in)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/bind-phone', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_bind_phone'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// ── User Restrictions / Ban ──
|
||||
|
||||
// List restrictions for a user (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/users/(?P<user_id>\d+)/restrictions', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_user_restrictions'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Add restriction (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/users/(?P<user_id>\d+)/restrict', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'add_user_restriction'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Remove restriction (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/users/restrictions/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'remove_user_restriction'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Restriction stats (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/users/restrictions/stats', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_restriction_stats'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET/POST /settings/mail-templates — 邮件内容模板
|
||||
*/
|
||||
public function handle_mail_templates(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$option_key = 'aksrt_mail_templates';
|
||||
$fields = [
|
||||
'welcome_subject', 'welcome_body',
|
||||
'comment_notify_subject', 'comment_notify_body',
|
||||
'comment_reply_subject', 'comment_reply_body',
|
||||
'password_reset_subject', 'password_reset_body',
|
||||
'email_verify_subject', 'email_verify_body',
|
||||
];
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$stored = get_option($option_key, []);
|
||||
if (!is_array($stored)) $stored = [];
|
||||
$result = [];
|
||||
foreach ($fields as $f) {
|
||||
$result[$f] = sanitize_text_field((string) ($stored[$f] ?? ''));
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
// POST
|
||||
$params = $this->get_json_params($request);
|
||||
$clean = [];
|
||||
foreach ($fields as $f) {
|
||||
$clean[$f] = wp_kses_post((string) ($params[$f] ?? ''));
|
||||
}
|
||||
update_option($option_key, $clean);
|
||||
|
||||
return $this->success(['message' => __('邮件模板已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_credit_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$config = \AKSRT\Settings\CreditsRuntime::get_credit_settings();
|
||||
return $this->success(['data' => $config]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$current = get_option('aksrt_credit_settings', []);
|
||||
if (!is_array($current)) {
|
||||
$current = [];
|
||||
}
|
||||
$merged = array_replace_recursive($current, $params);
|
||||
update_option('aksrt_credit_settings', $merged);
|
||||
|
||||
return $this->success(['message' => __('积分配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_auth_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'auth');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = \AKSRT\Settings\Settings::get_all();
|
||||
$invite = class_exists('\AKSRT\Settings\CreditsRuntime')
|
||||
? \AKSRT\Settings\CreditsRuntime::get_invite_registration_config()
|
||||
: [];
|
||||
|
||||
return $this->success(['data' => [
|
||||
'registration_email_verification_enabled' => \AKSRT\Settings\Settings::registration_requires_email_verification(),
|
||||
'registration_email_verification_method' => \AKSRT\Settings\Settings::registration_verification_method(),
|
||||
'invite_registration_enabled' => $invite['enabled'] ?? true,
|
||||
'invite_code_length' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::invite_code_length() : 6,
|
||||
'invite_register_field' => $invite['register_field'] ?? 'optional',
|
||||
'invite_reject_invalid' => $invite['reject_invalid'] ?? false,
|
||||
] + array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$sanitized = $this->sanitize_fields($params, $allowed);
|
||||
$settings = array_merge($settings, $sanitized);
|
||||
|
||||
if (array_key_exists('invite_registration_enabled', $params)) {
|
||||
$settings['invite_registration_enabled'] = !empty($params['invite_registration_enabled']);
|
||||
}
|
||||
if (array_key_exists('invite_code_length', $params)) {
|
||||
$settings['invite_code_length'] = max(4, min(12, (int) $params['invite_code_length']));
|
||||
}
|
||||
if (array_key_exists('invite_register_field', $params)) {
|
||||
$field = (string) $params['invite_register_field'];
|
||||
$settings['invit_code_mode'] = $field === 'required' ? 'must' : ($field === 'hidden' ? 'close' : 'open');
|
||||
}
|
||||
if (array_key_exists('invite_reject_invalid', $params)) {
|
||||
$reward = is_array($settings['invite_reward'] ?? null) ? $settings['invite_reward'] : [];
|
||||
$reward['reject_invalid'] = !empty($params['invite_reject_invalid']);
|
||||
$settings['invite_reward'] = $reward;
|
||||
}
|
||||
if (array_key_exists('registration_email_verification_enabled', $params)) {
|
||||
$settings['registration_email_verification_enabled'] = !empty($params['registration_email_verification_enabled']);
|
||||
}
|
||||
if (array_key_exists('registration_email_verification_method', $params)) {
|
||||
$method = (string) $params['registration_email_verification_method'];
|
||||
$settings['registration_email_verification_method'] = in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
}
|
||||
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('注册登录配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_notification_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'notifications');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = \AKSRT\Settings\Settings::get_all();
|
||||
return $this->success(['data' => array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('消息通知配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_get_notification_overview(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(100, max(1, (int) $request->get_param('per_page')));
|
||||
$type = sanitize_key((string) $request->get_param('type'));
|
||||
$is_read = $request->get_param('is_read');
|
||||
|
||||
$items = [];
|
||||
$unread = 0;
|
||||
if (class_exists('\AKSRT\Messages\Message')) {
|
||||
$args = ['limit' => $per_page, 'offset' => ($page - 1) * $per_page];
|
||||
if ($type !== '') {
|
||||
$args['type'] = $type;
|
||||
}
|
||||
$items = \AKSRT\Messages\Message::get_instance()->get_notifications(get_current_user_id(), $args);
|
||||
if ($is_read !== null && $is_read !== '') {
|
||||
$want_read = (string) $is_read === '1';
|
||||
$items = array_values(array_filter($items, static function ($item) use ($want_read) {
|
||||
return !empty($item['is_read']) === $want_read;
|
||||
}));
|
||||
}
|
||||
$unread = \AKSRT\Messages\Message::get_instance()->get_unread_count(get_current_user_id());
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$types = [];
|
||||
$today = 0;
|
||||
$system = 0;
|
||||
foreach ($items as $item) {
|
||||
$itemType = sanitize_key((string) ($item['type'] ?? 'system'));
|
||||
$types[$itemType] = ($types[$itemType] ?? 0) + 1;
|
||||
if ($itemType === 'system' || $itemType === 'notice') {
|
||||
$system++;
|
||||
}
|
||||
$created = (string) ($item['created_at'] ?? '');
|
||||
if ($created !== '' && str_starts_with($created, gmdate('Y-m-d'))) {
|
||||
$today++;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['data' => [
|
||||
'recent' => $items,
|
||||
'types' => array_map(static fn($key, $count) => ['type' => $key, 'count' => $count], array_keys($types), array_values($types)),
|
||||
'summary' => [
|
||||
'total' => $total,
|
||||
'unread' => $unread,
|
||||
'today' => $today,
|
||||
'system' => $system,
|
||||
],
|
||||
'pagination' => [
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_items' => $total,
|
||||
'total_pages' => 1,
|
||||
],
|
||||
]]);
|
||||
}
|
||||
|
||||
public function rest_mark_notification_read(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Messages\Message')) {
|
||||
return $this->success(['message' => __('消息系统未启用', 'aksrt')]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$msg = \AKSRT\Messages\Message::get_instance();
|
||||
if (!empty($params['mark_all'])) {
|
||||
$msg->mark_all_read(get_current_user_id());
|
||||
} else {
|
||||
$id = sanitize_text_field((string) ($params['id'] ?? ''));
|
||||
if ($id !== '') {
|
||||
$msg->mark_read(get_current_user_id(), $id);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('已更新消息状态', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_delete_notification_item(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
$params = $this->get_json_params($request);
|
||||
$id = sanitize_text_field((string) ($params['id'] ?? $request->get_param('id') ?? ''));
|
||||
if ($id === '') {
|
||||
return $this->error(__('参数错误', 'aksrt'), 400);
|
||||
}
|
||||
if (function_exists('aksrt_delete_user_notification')) {
|
||||
aksrt_delete_user_notification($user_id, $id);
|
||||
} elseif (class_exists('\AKSRT\Messages\Message')) {
|
||||
\AKSRT\Messages\Message::get_instance()->mark_read($user_id, $id);
|
||||
}
|
||||
return $this->success([
|
||||
'message' => __('消息已删除', 'aksrt'),
|
||||
'unread' => function_exists('aksrt_get_user_unread_notification_count')
|
||||
? aksrt_get_user_unread_notification_count($user_id)
|
||||
: 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_run_notification_cleanup(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->success(['message' => __('消息清理任务已受理', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_oauth(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$provider = SchemaProvider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'oauth');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = \AKSRT\Settings\Settings::get_all();
|
||||
return $this->success(['data' => array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('第三方登录配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_oauth_proxy(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$keys = [
|
||||
'oauth_proxy_mode',
|
||||
'oauth_proxy_secret',
|
||||
'oauth_proxy_server_url',
|
||||
'oauth_proxy_server_providers',
|
||||
'oauth_proxy_client_providers',
|
||||
'oauth_proxy_gzh_type',
|
||||
];
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$all = \AKSRT\Settings\Settings::get_all();
|
||||
$data = [];
|
||||
foreach ($keys as $key) {
|
||||
$data[$key] = $all[$key] ?? null;
|
||||
}
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $params)) {
|
||||
continue;
|
||||
}
|
||||
$settings[$key] = is_array($params[$key])
|
||||
? array_values(array_map('sanitize_key', (array) $params[$key]))
|
||||
: sanitize_text_field((string) $params[$key]);
|
||||
}
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('OAuth 代理配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_upload_comment_image(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
if (empty($_FILES['image']) && empty($_FILES['file'])) {
|
||||
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
|
||||
}
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$file_key = !empty($_FILES['image']) ? 'image' : 'file';
|
||||
$attachment_id = media_handle_upload($file_key, 0);
|
||||
if (is_wp_error($attachment_id)) {
|
||||
return $this->error($attachment_id->get_error_message(), 400);
|
||||
}
|
||||
$url = wp_get_attachment_url((int) $attachment_id);
|
||||
if (!$url) {
|
||||
return $this->error(__('上传失败', 'aksrt'), 500);
|
||||
}
|
||||
return $this->success([
|
||||
'message' => __('图片上传成功', 'aksrt'),
|
||||
'data' => [
|
||||
'url' => $url,
|
||||
'attachment_id' => (int) $attachment_id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_get_credits_me(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
return $this->success(['data' => [
|
||||
'user_id' => $user_id,
|
||||
'balance' => class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($user_id) : 0,
|
||||
]]);
|
||||
}
|
||||
|
||||
public function rest_list_credit_users(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$args = [
|
||||
'search' => (string) $request->get_param('search'),
|
||||
'page' => (int) $request->get_param('page'),
|
||||
'per_page' => (int) $request->get_param('per_page'),
|
||||
];
|
||||
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::list_users($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_adjust_user_credits(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return $this->error(__('积分系统不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$delta = (int) ($params['delta'] ?? 0);
|
||||
$reason = sanitize_text_field((string) ($params['reason'] ?? 'admin'));
|
||||
|
||||
if ($user_id <= 0 || $delta === 0) {
|
||||
return $this->error(__('用户或积分变动值无效', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if ($delta > 0) {
|
||||
$result = \AKSRT\Credits\Credits::add($user_id, $delta, $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
|
||||
} else {
|
||||
$result = \AKSRT\Credits\Credits::deduct($user_id, abs($delta), $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
|
||||
}
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message(), 422);
|
||||
}
|
||||
|
||||
return $this->success(['data' => [
|
||||
'user_id' => $user_id,
|
||||
'balance' => (int) $result,
|
||||
], 'message' => __('积分调整已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_get_credits_log(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$args = [
|
||||
'user_id' => (int) $request->get_param('user_id'),
|
||||
'reason' => (string) $request->get_param('reason'),
|
||||
'page' => (int) $request->get_param('page'),
|
||||
'per_page' => (int) $request->get_param('per_page'),
|
||||
'date_from' => (string) $request->get_param('date_from'),
|
||||
'date_to' => (string) $request->get_param('date_to'),
|
||||
];
|
||||
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::get_log($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerAuthPhoneOperations {
|
||||
|
||||
/**
|
||||
* POST /auth/modal-login — 弹窗登录
|
||||
*/
|
||||
public function modal_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$payload = is_array($params) ? $params : [];
|
||||
$login = sanitize_text_field((string) ($payload['log'] ?? $payload['login'] ?? ''));
|
||||
$password = (string) ($payload['pwd'] ?? $payload['password'] ?? '');
|
||||
|
||||
if ($login === '' || $password === '') {
|
||||
return $this->error('请输入用户名和密码');
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用', 500);
|
||||
}
|
||||
|
||||
$result = \AKSRT\Auth\Service::login_user([
|
||||
...$payload,
|
||||
'log' => $login,
|
||||
'pwd' => $password,
|
||||
'rememberme' => !empty($payload['rememberme']) ? (string) $payload['rememberme'] : 'forever',
|
||||
], true, true);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
$error = $result['error'] ?? '登录失败';
|
||||
$verify_url = $result['verify_url'] ?? '';
|
||||
$response = ['success' => false, 'message' => $error];
|
||||
if ($verify_url) {
|
||||
$response['verify_url'] = $verify_url;
|
||||
}
|
||||
return new \WP_REST_Response($response, 401);
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
return $this->success([
|
||||
'message' => __('登录成功', 'aksrt'),
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'user' => [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/modal-register — 弹窗注册
|
||||
*/
|
||||
public function modal_register(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$payload = is_array($params) ? $params : [];
|
||||
$username = sanitize_user((string) ($payload['user_login'] ?? $payload['username'] ?? ''), true);
|
||||
$email = sanitize_email((string) ($payload['user_email'] ?? $payload['email'] ?? ''));
|
||||
$password = (string) ($payload['user_pass'] ?? $payload['password'] ?? '');
|
||||
$confirm = (string) ($payload['user_pass_confirm'] ?? $payload['password_confirm'] ?? $payload['confirm_password'] ?? $password);
|
||||
$invite_code = sanitize_text_field((string) ($payload['invite_code'] ?? ''));
|
||||
$register_nonce = sanitize_text_field((string) ($payload['_register_nonce'] ?? ''));
|
||||
$agree_terms = !empty($payload['agree_terms']) ? '1' : '';
|
||||
|
||||
if ($username === '' || $email === '' || $password === '') {
|
||||
return $this->error('请填写所有必填项');
|
||||
}
|
||||
|
||||
if (!is_email($email)) {
|
||||
return $this->error('邮箱格式不正确');
|
||||
}
|
||||
|
||||
if (strlen($password) < 6) {
|
||||
return $this->error('密码至少 6 位');
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用', 500);
|
||||
}
|
||||
|
||||
$payload['user_login'] = $username;
|
||||
$payload['user_email'] = $email;
|
||||
$payload['user_pass'] = $password;
|
||||
$payload['user_pass_confirm'] = $confirm;
|
||||
$payload['invite_code'] = $invite_code;
|
||||
$payload['_register_nonce'] = $register_nonce;
|
||||
$payload['agree_terms'] = $agree_terms;
|
||||
|
||||
$result = \AKSRT\Auth\Service::register_user($payload);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '注册失败');
|
||||
}
|
||||
|
||||
$verification_required = !empty($result['verification_required']);
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
|
||||
$registered_user = null;
|
||||
$current_user = wp_get_current_user();
|
||||
if (
|
||||
$current_user instanceof \WP_User
|
||||
&& $current_user->ID > 0
|
||||
&& !empty($result['user_id'])
|
||||
&& (int) $result['user_id'] === (int) $current_user->ID
|
||||
) {
|
||||
$registered_user = $current_user;
|
||||
}
|
||||
$user = $current_user;
|
||||
$needs_verify = $verification_required;
|
||||
|
||||
return $this->success([
|
||||
'message' => $needs_verify ? __('注册成功,请验证邮箱', 'aksrt') : __('注册成功', 'aksrt'),
|
||||
'needs_verification' => $needs_verify,
|
||||
'verify_url' => $result['verify_url'] ?? '',
|
||||
'redirect' => $verification_required ? ($result['verify_url'] ?? '') : $login_url,
|
||||
'verification_required' => $verification_required,
|
||||
'login_url' => $login_url,
|
||||
'user_login' => $result['user_login'] ?? $username,
|
||||
'user_email' => $email,
|
||||
'user' => $registered_user ? [
|
||||
'id' => $registered_user->ID,
|
||||
'display_name' => $registered_user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($registered_user->ID, ['size' => 48])
|
||||
: get_avatar_url($registered_user->ID, ['size' => 48]),
|
||||
] : ($user->ID ? [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
] : null),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /auth/me — 当前用户信息(用于 AJAX 刷新)
|
||||
*/
|
||||
public function get_current_user_info(): \WP_REST_Response {
|
||||
$user = wp_get_current_user();
|
||||
if (!$user->ID) {
|
||||
return $this->error('未登录', 401);
|
||||
}
|
||||
|
||||
$unread = 0;
|
||||
if (class_exists('\AKSRT\Messages\PrivateMessage')) {
|
||||
$unread = \AKSRT\Messages\PrivateMessage::get_instance()->get_unread_count($user->ID);
|
||||
}
|
||||
|
||||
$credits = 0;
|
||||
if (class_exists('\AKSRT\Credits\Credits')) {
|
||||
$credits = \AKSRT\Credits\Credits::balance($user->ID);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'user_email' => $user->user_email,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
'is_admin' => current_user_can('manage_options'),
|
||||
'unread' => $unread,
|
||||
'credits' => $credits,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/send-sms-code — 发送短信验证码
|
||||
*/
|
||||
public function auth_send_sms_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$phone = sanitize_text_field((string) ($params['phone'] ?? ''));
|
||||
$scene = sanitize_key((string) ($params['scene'] ?? 'register'));
|
||||
if (!array_key_exists($scene, function_exists('aksrt_captcha_scenes') ? aksrt_captcha_scenes() : [])) {
|
||||
$scene = 'register';
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha($scene, is_array($params) ? $params : []);
|
||||
if ($captcha_result !== true) {
|
||||
return $this->error((string) $captcha_result, 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) {
|
||||
return $this->error('短信服务不可用');
|
||||
}
|
||||
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->send_code($phone);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'message' => $result['message'],
|
||||
'remaining_time' => $result['remaining_time'] ?? 60,
|
||||
]);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $result['message'],
|
||||
];
|
||||
if (isset($result['remaining_time'])) {
|
||||
$response['data'] = ['remaining_time' => $result['remaining_time']];
|
||||
}
|
||||
return new \WP_REST_Response($response, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-register — 手机号注册
|
||||
*/
|
||||
public function auth_phone_register(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::register_by_phone($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? '注册成功',
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'user_id' => $result['user_id'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '注册失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-login — 手机号 + 密码登录
|
||||
*/
|
||||
public function auth_phone_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::login_by_phone_password($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '登录失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-code-login — 手机号 + 验证码免密登录
|
||||
*/
|
||||
public function auth_phone_code_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::login_by_phone_code($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '登录失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/bind-phone — 绑定手机号
|
||||
*/
|
||||
public function auth_bind_phone(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::bind_phone($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success(['message' => $result['message']]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '绑定失败');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerFollowOperations {
|
||||
|
||||
/**
|
||||
* POST /follow/toggle — 切换关注状态
|
||||
*/
|
||||
public function follow_toggle(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\User\Follow')) {
|
||||
return $this->error('关注系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$target_user_id = (int) ($params['target_user_id'] ?? 0);
|
||||
$current_user_id = get_current_user_id();
|
||||
|
||||
if ($target_user_id <= 0 || $target_user_id === $current_user_id) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$target_user = get_userdata($target_user_id);
|
||||
if (!$target_user) {
|
||||
return $this->error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$is_following = \AKSRT\User\Follow::get_instance()->toggle($current_user_id, $target_user_id);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'is_following' => $is_following,
|
||||
'follower_count' => \AKSRT\User\Follow::get_instance()->get_follower_count($target_user_id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /follow/status — 查询关注状态
|
||||
*/
|
||||
public function follow_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\User\Follow')) {
|
||||
return $this->success(['data' => ['is_following' => false]]);
|
||||
}
|
||||
|
||||
$target_user_id = (int) $request->get_param('target_user_id');
|
||||
$current_user_id = get_current_user_id();
|
||||
|
||||
if ($target_user_id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$is_following = \AKSRT\User\Follow::get_instance()->is_following($current_user_id, $target_user_id);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'is_following' => $is_following,
|
||||
'follower_count' => \AKSRT\User\Follow::get_instance()->get_follower_count($target_user_id),
|
||||
'following_count' => \AKSRT\User\Follow::get_instance()->get_following_count($target_user_id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /follow/counts — 获取关注/粉丝数
|
||||
*/
|
||||
public function follow_counts(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\User\Follow')) {
|
||||
return $this->success(['data' => ['followers' => 0, 'following' => 0]]);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('user_id');
|
||||
if ($user_id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'followers' => \AKSRT\User\Follow::get_instance()->get_follower_count($user_id),
|
||||
'following' => \AKSRT\User\Follow::get_instance()->get_following_count($user_id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin REST handlers for identity provider configuration.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
use AKSRT\Settings\IdentityAdmin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerIdentitySettings {
|
||||
public function get_identity_settings(): \WP_REST_Response {
|
||||
return $this->success([
|
||||
'data' => $this->identity_settings_response(IdentityAdmin::read()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$settings = IdentityAdmin::update($this->get_json_params($request));
|
||||
|
||||
return $this->success([
|
||||
'message' => __('身份认证服务商配置已保存', 'aksrt'),
|
||||
'data' => $this->identity_settings_response($settings),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$provider = sanitize_key((string) ($params['provider'] ?? ''));
|
||||
$result = IdentityAdmin::test($provider);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['message'] ?? __('配置检查失败', 'aksrt')), 422);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => (string) $result['message'],
|
||||
'data' => ['provider' => $provider, 'configured' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
private function identity_settings_response(array $settings): array {
|
||||
$settings['callback_url'] = [
|
||||
'aliyun' => rest_url(self::API_NAMESPACE . '/identity/callback/aliyun'),
|
||||
'tencent' => rest_url(self::API_NAMESPACE . '/identity/callback/tencent'),
|
||||
];
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerInviteOperations {
|
||||
|
||||
/**
|
||||
* GET /invite/codes — 邀请码列表(管理后台)
|
||||
*/
|
||||
public function get_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$source = sanitize_key((string) $request->get_param('source'));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$usermeta_table = $wpdb->usermeta;
|
||||
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
|
||||
|
||||
$invite_rows = [];
|
||||
if ($invite_table) {
|
||||
$records = $wpdb->get_results("SELECT * FROM {$invite_table}", ARRAY_A);
|
||||
foreach ((array) $records as $record) {
|
||||
$invite_rows[(string) $record['invite_code']] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$user_rows = $wpdb->get_results(
|
||||
"SELECT user_id, meta_value AS invite_code FROM {$usermeta_table} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
foreach ((array) $user_rows as $row) {
|
||||
$user_id = (int) $row['user_id'];
|
||||
$user = get_userdata($user_id);
|
||||
$code = sanitize_key((string) $row['invite_code']);
|
||||
$record = $invite_rows[$code] ?? null;
|
||||
|
||||
$item = [
|
||||
'row_key' => 'user:' . $user_id,
|
||||
'source_type' => 'user',
|
||||
'source_label' => __('用户邀请码', 'aksrt'),
|
||||
'user' => $this->build_admin_user_summary($user_id, $user),
|
||||
'invite_code' => $code,
|
||||
'status' => $record ? 'used' : 'active',
|
||||
'status_label' => $record ? __('已使用', 'aksrt') : __('可用', 'aksrt'),
|
||||
'created_at' => $user ? (string) $user->user_registered : '',
|
||||
'used_at' => $record['created_at'] ?? '',
|
||||
'note' => '',
|
||||
'batch_id' => '',
|
||||
'created_by' => null,
|
||||
'registration_status' => $record['status'] ?? '',
|
||||
'invitee' => $this->build_admin_user_summary((int) ($record['invitee_id'] ?? 0)),
|
||||
'can_bulk' => false,
|
||||
'actions' => ['disable'],
|
||||
];
|
||||
|
||||
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->load_standalone_invite_codes() as $entry) {
|
||||
$code = sanitize_key((string) ($entry['code'] ?? ''));
|
||||
$record = $invite_rows[$code] ?? null;
|
||||
$used_by = (int) ($entry['used_by'] ?? 0);
|
||||
$disabled = !empty($entry['disabled']);
|
||||
$used = !empty($entry['used']) || $used_by > 0 || !empty($record['invitee_id']);
|
||||
$status_key = $disabled ? 'disabled' : ($used ? 'used' : 'active');
|
||||
$status_map = [
|
||||
'active' => __('可用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
];
|
||||
$invitee_id = $used_by > 0 ? $used_by : (int) ($record['invitee_id'] ?? 0);
|
||||
|
||||
$item = [
|
||||
'row_key' => 'standalone:' . $code,
|
||||
'source_type' => 'standalone',
|
||||
'source_label' => __('独立邀请码', 'aksrt'),
|
||||
'user' => null,
|
||||
'invite_code' => $code,
|
||||
'status' => $status_key,
|
||||
'status_label' => $status_map[$status_key],
|
||||
'created_at' => (string) ($entry['created_at'] ?? ''),
|
||||
'used_at' => (string) ($entry['used_at'] ?? ($record['created_at'] ?? '')),
|
||||
'note' => (string) ($entry['note'] ?? ''),
|
||||
'batch_id' => (string) ($entry['batch_id'] ?? ''),
|
||||
'created_by' => $this->build_admin_user_summary((int) ($entry['created_by'] ?? 0)),
|
||||
'registration_status' => $record['status'] ?? '',
|
||||
'invitee' => $this->build_admin_user_summary($invitee_id),
|
||||
'can_bulk' => true,
|
||||
'actions' => ['disable', 'delete'],
|
||||
];
|
||||
|
||||
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
usort($items, static function ($left, $right) {
|
||||
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
|
||||
});
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$paged = array_slice($items, $offset, $per_page);
|
||||
$stats = $this->build_invite_operation_stats($items, $invite_rows);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values($paged),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
|
||||
'stats' => $stats,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('全部状态', 'aksrt'), 'value' => ''],
|
||||
['label' => __('可用', 'aksrt'), 'value' => 'active'],
|
||||
['label' => __('已使用', 'aksrt'), 'value' => 'used'],
|
||||
['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
|
||||
],
|
||||
'sources' => [
|
||||
['label' => __('全部来源', 'aksrt'), 'value' => ''],
|
||||
['label' => __('用户邀请码', 'aksrt'), 'value' => 'user'],
|
||||
['label' => __('独立邀请码', 'aksrt'), 'value' => 'standalone'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /invite/codes — 批量生成邀请码(管理后台)
|
||||
* Body: { count: int, length: int, prefix: string }
|
||||
* 为指定用户或批量创建邀请码。
|
||||
*/
|
||||
public function generate_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Credits\Invite')) {
|
||||
return $this->error(__('邀请系统未启用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$count = max(1, min(100, (int) ($params['count'] ?? 1)));
|
||||
$length = max(4, min(12, (int) ($params['length'] ?? 6)));
|
||||
$prefix = sanitize_key((string) ($params['prefix'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
$batch_id = sanitize_text_field((string) ($params['batch_id'] ?? ''));
|
||||
|
||||
if ($batch_id === '') {
|
||||
$batch_id = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
|
||||
}
|
||||
|
||||
$generated = [];
|
||||
$codes = $this->load_standalone_invite_codes();
|
||||
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = '';
|
||||
for ($attempt = 0; $attempt < 12; $attempt++) {
|
||||
$candidate = $prefix;
|
||||
for ($j = 0; $j < $length; $j++) {
|
||||
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
$candidate = sanitize_key($candidate);
|
||||
if (!$this->invite_code_exists_globally($candidate, $codes)) {
|
||||
$code = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$codes[] = [
|
||||
'code' => $code,
|
||||
'created_at' => current_time('mysql'),
|
||||
'used' => false,
|
||||
'used_by' => 0,
|
||||
'used_at' => '',
|
||||
'disabled' => false,
|
||||
'note' => $note,
|
||||
'batch_id' => $batch_id,
|
||||
'created_by' => get_current_user_id(),
|
||||
];
|
||||
$generated[] = $code;
|
||||
}
|
||||
|
||||
$this->save_standalone_invite_codes($codes);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf(__('已生成 %d 个邀请码', 'aksrt'), count($generated)),
|
||||
'data' => [
|
||||
'codes' => $generated,
|
||||
'batch_id' => $batch_id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /invite/codes/{id}/disable — 禁用邀请码
|
||||
*/
|
||||
public function disable_invite_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = (int) $request->get_param('id');
|
||||
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$removed = $this->remove_user_invite_code($user_id);
|
||||
if (!$removed) {
|
||||
return $this->error(__('邀请码不存在或已禁用', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('邀请码已禁用', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function bulk_update_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
||||
$targets = isset($params['targets']) && is_array($params['targets']) ? $params['targets'] : [];
|
||||
|
||||
if (!in_array($action, ['disable', 'delete'], true) || empty($targets)) {
|
||||
return $this->error(__('批量操作参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$codes = $this->load_standalone_invite_codes();
|
||||
$processed = 0;
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$source_type = sanitize_key((string) ($target['source_type'] ?? ''));
|
||||
if ($source_type === 'user') {
|
||||
$user_id = (int) ($target['user_id'] ?? 0);
|
||||
if ($action === 'disable' && $this->remove_user_invite_code($user_id)) {
|
||||
$processed++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($source_type !== 'standalone') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = sanitize_key((string) ($target['code'] ?? $target['invite_code'] ?? ''));
|
||||
foreach ($codes as $index => $entry) {
|
||||
$entry_code = sanitize_key((string) ($entry['code'] ?? ''));
|
||||
if ($entry_code !== $code) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && empty($entry['used']) && empty($entry['used_by'])) {
|
||||
unset($codes[$index]);
|
||||
} else {
|
||||
$entry['disabled'] = true;
|
||||
$codes[$index] = $entry;
|
||||
}
|
||||
$processed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->save_standalone_invite_codes($codes);
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf(__('已处理 %d 条邀请码记录', 'aksrt'), $processed),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /invite/stats — 邀请系统统计
|
||||
*/
|
||||
public function get_invite_stats(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$items = [];
|
||||
$user_records = $wpdb->get_results(
|
||||
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code'",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
foreach ((array) $user_records as $row) {
|
||||
$items[] = [
|
||||
'source_type' => 'user',
|
||||
'status' => 'active',
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->load_standalone_invite_codes() as $entry) {
|
||||
$used = !empty($entry['used']) || !empty($entry['used_by']);
|
||||
$disabled = !empty($entry['disabled']);
|
||||
$items[] = [
|
||||
'source_type' => 'standalone',
|
||||
'status' => $disabled ? 'disabled' : ($used ? 'used' : 'active'),
|
||||
];
|
||||
}
|
||||
|
||||
$stats = $this->build_invite_operation_stats($items);
|
||||
return $this->success(['data' => $stats]);
|
||||
}
|
||||
|
||||
private function load_standalone_invite_codes(): array {
|
||||
$codes = get_option('aksrt_standalone_invite_codes', []);
|
||||
return is_array($codes) ? array_values($codes) : [];
|
||||
}
|
||||
|
||||
private function save_standalone_invite_codes(array $codes): void {
|
||||
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
|
||||
}
|
||||
|
||||
private function invite_code_exists_globally(string $candidate, array $standalone_codes = []): bool {
|
||||
if ($candidate === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$existing_user_code = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
|
||||
$candidate
|
||||
));
|
||||
if ($existing_user_code) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($standalone_codes as $entry) {
|
||||
if (sanitize_key((string) ($entry['code'] ?? '')) === $candidate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
|
||||
if ($invite_table) {
|
||||
$existing_invite = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$invite_table} WHERE invite_code = %s LIMIT 1",
|
||||
$candidate
|
||||
));
|
||||
if ($existing_invite) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function invite_item_matches_filters(array $item, string $keyword, string $status, string $source, string $date_from, string $date_to): bool {
|
||||
if ($status !== '' && ($item['status'] ?? '') !== $status) {
|
||||
return false;
|
||||
}
|
||||
if ($source !== '' && ($item['source_type'] ?? '') !== $source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$created_at = (string) ($item['created_at'] ?? '');
|
||||
if ($date_from !== '' && $created_at !== '' && strcmp($created_at, $date_from . ' 00:00:00') < 0) {
|
||||
return false;
|
||||
}
|
||||
if ($date_to !== '' && $created_at !== '' && strcmp($created_at, $date_to . ' 23:59:59') > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($keyword === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$haystack = implode(' ', array_filter([
|
||||
(string) ($item['invite_code'] ?? ''),
|
||||
(string) ($item['status_label'] ?? ''),
|
||||
(string) ($item['note'] ?? ''),
|
||||
(string) ($item['batch_id'] ?? ''),
|
||||
(string) ($item['user']['display_name'] ?? ''),
|
||||
(string) ($item['invitee']['display_name'] ?? ''),
|
||||
(string) ($item['invitee']['email'] ?? ''),
|
||||
]));
|
||||
|
||||
return mb_stripos($haystack, $keyword) !== false;
|
||||
}
|
||||
|
||||
private function build_invite_operation_stats(array $items, array $invite_rows = []): array {
|
||||
$stats = [
|
||||
'total_codes' => count($items),
|
||||
'active_codes' => 0,
|
||||
'used_codes' => 0,
|
||||
'disabled_codes' => 0,
|
||||
'user_codes' => 0,
|
||||
'standalone_codes' => 0,
|
||||
'total_invites' => count($invite_rows),
|
||||
'total_rewarded' => 0,
|
||||
];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$status = $item['status'] ?? '';
|
||||
if ($status === 'active') {
|
||||
$stats['active_codes']++;
|
||||
} elseif ($status === 'used') {
|
||||
$stats['used_codes']++;
|
||||
} elseif ($status === 'disabled') {
|
||||
$stats['disabled_codes']++;
|
||||
}
|
||||
|
||||
if (($item['source_type'] ?? '') === 'user') {
|
||||
$stats['user_codes']++;
|
||||
} elseif (($item['source_type'] ?? '') === 'standalone') {
|
||||
$stats['standalone_codes']++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($invite_rows as $row) {
|
||||
if (($row['status'] ?? '') === 'rewarded') {
|
||||
$stats['total_rewarded']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function remove_user_invite_code(int $user_id): bool {
|
||||
$user_id = (int) $user_id;
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = get_user_meta($user_id, 'aksrt_invite_code', true);
|
||||
if (!$code) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete_user_meta($user_id, 'aksrt_invite_code');
|
||||
|
||||
$map = get_option('aksrt_invite_code_map', []);
|
||||
if (is_array($map) && isset($map[$code])) {
|
||||
unset($map[$code]);
|
||||
update_option('aksrt_invite_code_map', $map, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Logged-in user profile media uploads.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerProfileMediaOperations {
|
||||
public function rest_upload_user_avatar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->upload_profile_image(
|
||||
['avatar', 'file'],
|
||||
'aksrt_avatar_attachment_id',
|
||||
__('头像已更新', 'aksrt'),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function rest_delete_user_avatar(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
|
||||
$attachment_id = (int) get_user_meta($user_id, 'aksrt_avatar_attachment_id', true);
|
||||
delete_user_meta($user_id, 'aksrt_avatar_attachment_id');
|
||||
$this->delete_owned_profile_attachment($attachment_id, $user_id);
|
||||
do_action('aksrt_user_avatar_changed', $user_id, 0, '');
|
||||
|
||||
return $this->success([
|
||||
'message' => __('已恢复默认头像', 'aksrt'),
|
||||
'url' => function_exists('aksrt_get_default_avatar_url') ? aksrt_get_default_avatar_url() : '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_upload_user_background(\WP_REST_Request $request): \WP_REST_Response {
|
||||
return $this->upload_profile_image(
|
||||
['background', 'file'],
|
||||
'aksrt_background_attachment_id',
|
||||
__('背景图已更新', 'aksrt'),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $file_keys Accepted multipart field names.
|
||||
*/
|
||||
private function upload_profile_image(array $file_keys, string $meta_key, string $message, bool $is_avatar): \WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
|
||||
$center_settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_center_settings()
|
||||
: ['avatar_upload' => true, 'cover_upload' => true];
|
||||
if ($is_avatar && empty($center_settings['avatar_upload'])) {
|
||||
return $this->error(__('头像上传功能未开启', 'aksrt'), 403);
|
||||
}
|
||||
if (!$is_avatar && (empty($center_settings['cover_upload']) || !aksrt_get_setting('allow_background_upload', false))) {
|
||||
return $this->error(__('背景图上传功能未开启', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
$file_key = '';
|
||||
foreach ($file_keys as $candidate) {
|
||||
if (!empty($_FILES[$candidate])) {
|
||||
$file_key = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($file_key === '') {
|
||||
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$file = $_FILES[$file_key];
|
||||
$upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
|
||||
if ($upload_error !== UPLOAD_ERR_OK) {
|
||||
return $this->error(__('头像文件上传失败,请重新选择', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size_mb = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2)));
|
||||
$max_size = $max_size_mb * MB_IN_BYTES;
|
||||
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
|
||||
if ($actual_size <= 0 || $actual_size > $max_size) {
|
||||
return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413);
|
||||
}
|
||||
|
||||
$checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], [
|
||||
'jpg|jpeg|jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
]);
|
||||
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type'])) {
|
||||
return $this->error(__('仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$image_size = wp_getimagesize($file['tmp_name']);
|
||||
if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
|
||||
return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415);
|
||||
}
|
||||
|
||||
$attachment_id = media_handle_upload($file_key, 0, [], [
|
||||
'test_form' => false,
|
||||
'mimes' => [
|
||||
'jpg|jpeg|jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
],
|
||||
]);
|
||||
if (is_wp_error($attachment_id)) {
|
||||
return $this->error($attachment_id->get_error_message(), 400);
|
||||
}
|
||||
|
||||
$attachment_id = (int) $attachment_id;
|
||||
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
|
||||
if (!wp_attachment_is_image($attachment_id)) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
return $this->error(__('上传文件不是有效图片', 'aksrt'), 415);
|
||||
}
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_user_id', $user_id);
|
||||
update_post_meta($attachment_id, '_aksrt_profile_media_type', $is_avatar ? 'avatar' : 'background');
|
||||
|
||||
$url = wp_get_attachment_image_url($attachment_id, $is_avatar ? 'thumbnail' : 'full');
|
||||
if (!$url) {
|
||||
wp_delete_attachment($attachment_id, true);
|
||||
return $this->error(__('上传失败', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$previous_id = (int) get_user_meta($user_id, $meta_key, true);
|
||||
update_user_meta($user_id, $meta_key, $attachment_id);
|
||||
if (!$is_avatar) {
|
||||
update_user_meta($user_id, 'aksrt_background_image', esc_url_raw($url));
|
||||
}
|
||||
|
||||
if ($previous_id > 0 && $previous_id !== $attachment_id) {
|
||||
$this->delete_owned_profile_attachment($previous_id, $user_id);
|
||||
}
|
||||
|
||||
if ($is_avatar) {
|
||||
do_action('aksrt_user_avatar_changed', $user_id, $attachment_id, $url);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $message,
|
||||
'url' => $url,
|
||||
'data' => [
|
||||
'url' => $url,
|
||||
'attachment_id' => $attachment_id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function delete_owned_profile_attachment(int $attachment_id, int $user_id): bool {
|
||||
if ($attachment_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attachment = get_post($attachment_id);
|
||||
if (!$attachment instanceof \WP_Post || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$managed_user_id = (int) get_post_meta($attachment_id, '_aksrt_profile_media_user_id', true);
|
||||
if ($managed_user_id > 0 && $managed_user_id !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) wp_delete_attachment($attachment_id, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerReportOperations {
|
||||
|
||||
public function get_report_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => [],
|
||||
'reports' => [],
|
||||
'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0],
|
||||
'total' => 0,
|
||||
'page' => 1,
|
||||
'options' => [
|
||||
'statuses' => $this->get_report_status_options(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('report');
|
||||
if (!$table) {
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => [],
|
||||
'reports' => [],
|
||||
'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0],
|
||||
'total' => 0,
|
||||
'page' => 1,
|
||||
'options' => [
|
||||
'statuses' => $this->get_report_status_options(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: $request->get_param('limit') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$category = sanitize_key((string) $request->get_param('category'));
|
||||
$target_type = sanitize_key((string) $request->get_param('target_type'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($category !== '') {
|
||||
$where[] = 'category = %s';
|
||||
$params[] = $category;
|
||||
}
|
||||
if ($target_type !== '') {
|
||||
$where[] = 'target_type = %s';
|
||||
$params[] = $target_type;
|
||||
}
|
||||
if ($date_from !== '') {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = $date_from . ' 00:00:00';
|
||||
}
|
||||
if ($date_to !== '') {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = $date_to . ' 23:59:59';
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_report_admin_item($row);
|
||||
if ($keyword !== '' && !$this->report_matches_keyword($item, $keyword)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$paged = array_slice($items, $offset, $per_page);
|
||||
$counts = \AKSRT\Messages\ReportCenter::get_instance()->get_status_counts();
|
||||
$categories = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$cat = sanitize_key((string) ($row['category'] ?? ''));
|
||||
if ($cat !== '') {
|
||||
$categories[$cat] = $cat;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values($paged),
|
||||
'reports' => array_values($paged),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'counts' => $counts,
|
||||
'options' => [
|
||||
'statuses' => $this->get_report_status_options(),
|
||||
'actions' => $this->get_report_action_options(),
|
||||
'targets' => $this->get_report_target_type_options(),
|
||||
'categories' => array_map(static function ($item) {
|
||||
return ['label' => $item, 'value' => $item];
|
||||
}, array_values($categories)),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update_report_status(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
return $this->error(__('举报系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$report_id = (int) ($params['report_id'] ?? 0);
|
||||
if ($report_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->handle_report_process($report_id, $params);
|
||||
}
|
||||
|
||||
public function get_report_counts(): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
return $this->success(['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0]);
|
||||
}
|
||||
return $this->success(\AKSRT\Messages\ReportCenter::get_instance()->get_status_counts());
|
||||
}
|
||||
|
||||
public function get_report_detail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$report_id = (int) $request->get_param('id');
|
||||
$table = \AKSRT\Core\DBMigrator::table('report');
|
||||
|
||||
if ($report_id <= 0 || !$table) {
|
||||
return $this->error(__('举报工单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('举报工单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$related = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE target_type = %s AND target_id = %d",
|
||||
sanitize_key((string) $row['target_type']),
|
||||
(int) $row['target_id']
|
||||
));
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'report' => $this->build_report_admin_item($row),
|
||||
'related' => ['same_target_count' => $related],
|
||||
'options' => [
|
||||
'statuses' => $this->get_report_status_options(),
|
||||
'actions' => $this->get_report_action_options(sanitize_key((string) $row['target_type'])),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function process_report(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$report_id = (int) $request->get_param('id');
|
||||
if ($report_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->handle_report_process($report_id, $this->get_json_params($request));
|
||||
}
|
||||
|
||||
private function handle_report_process(int $report_id, array $payload): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('report');
|
||||
if (!$table) {
|
||||
return $this->error(__('举报系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($payload['status'] ?? ''));
|
||||
$action_taken = sanitize_key((string) ($payload['action_taken'] ?? 'none'));
|
||||
$note = sanitize_textarea_field((string) ($payload['note'] ?? ''));
|
||||
|
||||
$valid_statuses = array_column($this->get_report_status_options(), 'value');
|
||||
$valid_actions = array_column($this->get_report_action_options(), 'value');
|
||||
|
||||
if ($status === '' || !in_array($status, $valid_statuses, true)) {
|
||||
return $this->error(__('处理状态无效', 'aksrt'));
|
||||
}
|
||||
if (!in_array($action_taken, $valid_actions, true)) {
|
||||
$action_taken = 'none';
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('举报工单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$effect = $this->apply_report_action(
|
||||
sanitize_key((string) $row['target_type']),
|
||||
(int) $row['target_id'],
|
||||
$action_taken
|
||||
);
|
||||
|
||||
$notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
|
||||
if (!is_array($notes)) {
|
||||
$notes = [];
|
||||
}
|
||||
$notes[] = [
|
||||
'note' => $note,
|
||||
'status' => $status,
|
||||
'action_taken' => $action_taken,
|
||||
'operator' => get_current_user_id(),
|
||||
'timestamp' => current_time('mysql'),
|
||||
'effect' => $effect,
|
||||
];
|
||||
|
||||
$updated = $wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'status' => $status,
|
||||
'updated_at' => current_time('mysql'),
|
||||
'notes_json' => wp_json_encode($notes),
|
||||
],
|
||||
['id' => $report_id],
|
||||
['%s', '%s', '%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
if ($updated === false) {
|
||||
return $this->error(__('处理失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('举报工单已处理', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_report_admin_item(array $row): array {
|
||||
$reporter = get_userdata((int) ($row['reporter_id'] ?? 0));
|
||||
$notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
|
||||
if (!is_array($notes)) {
|
||||
$notes = [];
|
||||
}
|
||||
|
||||
$normalized_notes = [];
|
||||
foreach ($notes as $entry) {
|
||||
$operator_id = (int) ($entry['operator'] ?? 0);
|
||||
$operator_user = $operator_id > 0 ? get_userdata($operator_id) : null;
|
||||
$normalized_notes[] = [
|
||||
'note' => (string) ($entry['note'] ?? ''),
|
||||
'status' => sanitize_key((string) ($entry['status'] ?? '')),
|
||||
'action_taken' => sanitize_key((string) ($entry['action_taken'] ?? 'none')),
|
||||
'operator' => $operator_id,
|
||||
'operator_name' => $operator_user ? $operator_user->display_name : '',
|
||||
'timestamp' => (string) ($entry['timestamp'] ?? ''),
|
||||
'effect' => (string) ($entry['effect'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
$last_note = !empty($normalized_notes) ? $normalized_notes[count($normalized_notes) - 1] : null;
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'target_type' => sanitize_key((string) ($row['target_type'] ?? '')),
|
||||
'target_id' => (int) ($row['target_id'] ?? 0),
|
||||
'reason' => (string) ($row['reason'] ?? ''),
|
||||
'category' => sanitize_key((string) ($row['category'] ?? '')),
|
||||
'status' => sanitize_key((string) ($row['status'] ?? 'pending')),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
'reporter' => $this->build_admin_user_summary($reporter ? (int) $reporter->ID : 0, $reporter),
|
||||
'target' => $this->build_report_target_summary(
|
||||
sanitize_key((string) ($row['target_type'] ?? '')),
|
||||
(int) ($row['target_id'] ?? 0)
|
||||
),
|
||||
'notes' => $normalized_notes,
|
||||
'note_count' => count($normalized_notes),
|
||||
'last_note' => $last_note,
|
||||
];
|
||||
}
|
||||
|
||||
private function report_matches_keyword(array $item, string $keyword): bool {
|
||||
$haystack = implode(' ', array_filter([
|
||||
(string) ($item['reason'] ?? ''),
|
||||
(string) ($item['category'] ?? ''),
|
||||
(string) ($item['reporter']['display_name'] ?? ''),
|
||||
(string) ($item['reporter']['email'] ?? ''),
|
||||
(string) ($item['target']['title'] ?? ''),
|
||||
(string) ($item['target']['display_name'] ?? ''),
|
||||
(string) ($item['target']['excerpt'] ?? ''),
|
||||
(string) ($item['target_id'] ?? ''),
|
||||
]));
|
||||
|
||||
return mb_stripos($haystack, $keyword) !== false;
|
||||
}
|
||||
|
||||
private function build_report_target_summary(string $target_type, int $target_id): array {
|
||||
$summary = [
|
||||
'type' => $target_type,
|
||||
'id' => $target_id,
|
||||
'title' => '',
|
||||
'excerpt' => '',
|
||||
'display_name' => '',
|
||||
'status' => '',
|
||||
'url' => '',
|
||||
'ban_state' => '',
|
||||
];
|
||||
|
||||
if ($target_type === 'post') {
|
||||
$post = get_post($target_id);
|
||||
if ($post instanceof \WP_Post) {
|
||||
$summary['title'] = get_the_title($post);
|
||||
$summary['status'] = (string) $post->post_status;
|
||||
$summary['url'] = get_permalink($post);
|
||||
}
|
||||
} elseif ($target_type === 'comment') {
|
||||
$comment = get_comment($target_id);
|
||||
if ($comment instanceof \WP_Comment) {
|
||||
$summary['display_name'] = (string) $comment->comment_author;
|
||||
$summary['excerpt'] = wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 24);
|
||||
$summary['status'] = $comment->comment_approved === '1' ? 'approved' : (string) $comment->comment_approved;
|
||||
$summary['url'] = get_comment_link($comment);
|
||||
if (!empty($comment->comment_post_ID)) {
|
||||
$summary['title'] = get_the_title((int) $comment->comment_post_ID);
|
||||
}
|
||||
}
|
||||
} elseif ($target_type === 'user') {
|
||||
$user = get_userdata($target_id);
|
||||
if ($user instanceof \WP_User) {
|
||||
$summary['display_name'] = $user->display_name;
|
||||
$summary['title'] = $user->display_name;
|
||||
$summary['url'] = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : '';
|
||||
$summary['ban_state'] = (string) get_user_meta($target_id, 'aksrt_user_ban_state', true);
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function apply_report_action(string $target_type, int $target_id, string $action_taken): string {
|
||||
if ($target_id <= 0 || $action_taken === 'none') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($target_type === 'user') {
|
||||
if (in_array($action_taken, ['muted', 'restricted', 'banned'], true)) {
|
||||
update_user_meta($target_id, 'aksrt_user_ban_state', $action_taken);
|
||||
return sprintf(__('已同步用户限制状态:%s', 'aksrt'), $action_taken);
|
||||
}
|
||||
|
||||
if ($action_taken === 'clear') {
|
||||
delete_user_meta($target_id, 'aksrt_user_ban_state');
|
||||
return __('已清除用户限制状态', 'aksrt');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
|
||||
wp_update_post([
|
||||
'ID' => $target_id,
|
||||
'post_status' => 'pending',
|
||||
]);
|
||||
return __('已将内容改为待审状态', 'aksrt');
|
||||
}
|
||||
|
||||
if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
|
||||
wp_set_comment_status($target_id, 'hold', true);
|
||||
return __('已将评论改为待审状态', 'aksrt');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function get_report_status_options(): array {
|
||||
return [
|
||||
['label' => __('待处理', 'aksrt'), 'value' => 'pending'],
|
||||
['label' => __('处理中', 'aksrt'), 'value' => 'reviewing'],
|
||||
['label' => __('已解决', 'aksrt'), 'value' => 'resolved'],
|
||||
['label' => __('已驳回', 'aksrt'), 'value' => 'dismissed'],
|
||||
];
|
||||
}
|
||||
|
||||
private function get_report_target_type_options(): array {
|
||||
return [
|
||||
['label' => __('文章', 'aksrt'), 'value' => 'post'],
|
||||
['label' => __('评论', 'aksrt'), 'value' => 'comment'],
|
||||
['label' => __('用户', 'aksrt'), 'value' => 'user'],
|
||||
];
|
||||
}
|
||||
|
||||
private function get_report_action_options(string $target_type = ''): array {
|
||||
$common = [
|
||||
['label' => __('仅更新状态', 'aksrt'), 'value' => 'none'],
|
||||
['label' => __('升级人工处理', 'aksrt'), 'value' => 'escalated'],
|
||||
];
|
||||
|
||||
if ($target_type === 'user' || $target_type === '') {
|
||||
$common = array_merge($common, [
|
||||
['label' => __('发送警告', 'aksrt'), 'value' => 'warned'],
|
||||
['label' => __('禁言', 'aksrt'), 'value' => 'muted'],
|
||||
['label' => __('限制功能', 'aksrt'), 'value' => 'restricted'],
|
||||
['label' => __('封禁账号', 'aksrt'), 'value' => 'banned'],
|
||||
['label' => __('解除限制', 'aksrt'), 'value' => 'clear'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($target_type === 'post' || $target_type === 'comment' || $target_type === '') {
|
||||
$common[] = ['label' => __('转为待审内容', 'aksrt'), 'value' => 'content_removed'];
|
||||
}
|
||||
|
||||
return $common;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /report/submit — 前台举报提交(登录用户)
|
||||
*/
|
||||
public function submit_report(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
return $this->error('举报系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$target_type = sanitize_key((string) ($params['target_type'] ?? ''));
|
||||
$target_id = (int) ($params['target_id'] ?? 0);
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
$category = sanitize_key((string) ($params['category'] ?? ''));
|
||||
|
||||
if ($target_type === '' || $target_id <= 0 || $reason === '') {
|
||||
return $this->error('请填写完整的举报信息');
|
||||
}
|
||||
|
||||
$valid_types = ['post', 'comment', 'user'];
|
||||
if (!in_array($target_type, $valid_types, true)) {
|
||||
return $this->error('无效的举报类型');
|
||||
}
|
||||
|
||||
$report_id = \AKSRT\Messages\ReportCenter::get_instance()->submit(
|
||||
get_current_user_id(),
|
||||
$target_type,
|
||||
$target_id,
|
||||
$reason,
|
||||
$category
|
||||
);
|
||||
|
||||
if ($report_id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('举报已提交,我们会尽快处理', 'aksrt'),
|
||||
'data' => ['report_id' => $report_id],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error(__('举报提交失败', 'aksrt'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerRestrictionOperations {
|
||||
|
||||
public function get_user_restrictions(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = (int) $request->get_param('user_id');
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return $this->error('用户不存在', 404);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\User\Restriction')) {
|
||||
return $this->success(['data' => ['restrictions' => []]]);
|
||||
}
|
||||
|
||||
$restrictions = \AKSRT\User\Restriction::get_instance()->get_user_restrictions($user_id, false);
|
||||
return $this->success(['data' => ['restrictions' => $restrictions]]);
|
||||
}
|
||||
|
||||
public function add_user_restriction(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$user_id = (int) $request->get_param('user_id');
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return $this->error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$params = $request->get_json_params() ?? [];
|
||||
$type = sanitize_key((string) ($params['type'] ?? ''));
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
$scope = sanitize_key((string) ($params['scope'] ?? 'global'));
|
||||
$expires_at = sanitize_text_field((string) ($params['expires_at'] ?? ''));
|
||||
|
||||
if (!$type) {
|
||||
return $this->error('请选择限制类型');
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\User\Restriction')) {
|
||||
return $this->error('限制系统未启用', 501);
|
||||
}
|
||||
|
||||
$result = \AKSRT\User\Restriction::get_instance()->restrict(
|
||||
$user_id, $type, $reason, $scope, $expires_at, get_current_user_id()
|
||||
);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->error($result['message']);
|
||||
}
|
||||
|
||||
return $this->success(['message' => '限制已添加', 'data' => $result]);
|
||||
}
|
||||
|
||||
public function remove_user_restriction(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$restriction_id = (int) $request->get_param('id');
|
||||
|
||||
if (!class_exists('\AKSRT\User\Restriction')) {
|
||||
return $this->error('限制系统未启用', 501);
|
||||
}
|
||||
|
||||
$result = \AKSRT\User\Restriction::get_instance()->remove_restriction($restriction_id);
|
||||
if (!$result) {
|
||||
return $this->error('移除失败', 500);
|
||||
}
|
||||
|
||||
return $this->success(['message' => '限制已移除']);
|
||||
}
|
||||
|
||||
public function get_restriction_stats(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\User\Restriction')) {
|
||||
return $this->success(['data' => ['total' => 0, 'active' => 0, 'banned' => 0]]);
|
||||
}
|
||||
|
||||
$stats = \AKSRT\User\Restriction::get_instance()->get_stats();
|
||||
return $this->success(['data' => $stats]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerVerifyIntegrations {
|
||||
|
||||
/**
|
||||
* GET /sms/status — SMS provider connection status.
|
||||
*/
|
||||
public function get_sms_status(): \WP_REST_Response {
|
||||
if (class_exists('\AKSRT\Sms\SMS')) {
|
||||
return $this->success(['data' => \AKSRT\Sms\SMS::get_instance()->get_provider_status()]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'enabled' => false,
|
||||
'provider' => 'none',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
'verified' => false,
|
||||
'transport' => '',
|
||||
'missing_fields' => [],
|
||||
'code_length' => 6,
|
||||
'code_expire' => 300,
|
||||
'rate_limit' => 60,
|
||||
'daily_limit' => 10,
|
||||
'notify_bind' => true,
|
||||
'notify_login' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sms/test — Send a test SMS to verify provider configuration.
|
||||
*/
|
||||
public function test_sms_send(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) {
|
||||
return $this->error('短信服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$phone = sanitize_text_field((string) ($params['phone'] ?? ''));
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return $this->error('手机号格式不正确');
|
||||
}
|
||||
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->test_provider($phone);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success(['message' => $result['message'] ?? '发送成功']);
|
||||
}
|
||||
|
||||
return $this->error($result['message'] ?? '发送失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /wechat/status — WeChat notification service status.
|
||||
*/
|
||||
public function get_wechat_status(): \WP_REST_Response {
|
||||
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||
return $this->success(['data' => \AKSRT\Payment\WeChatNotify::get_instance()->get_status()]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'appid' => '',
|
||||
'configured' => false,
|
||||
'has_token' => false,
|
||||
'available' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /wechat/settings — 更新微信通知设置
|
||||
*/
|
||||
public function update_wechat_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
|
||||
$settings = [
|
||||
'enabled' => !empty($params['enabled']),
|
||||
'app_id' => sanitize_text_field((string) ($params['app_id'] ?? '')),
|
||||
'app_secret' => sanitize_text_field((string) ($params['app_secret'] ?? '')),
|
||||
'template_id' => sanitize_text_field((string) ($params['template_id'] ?? '')),
|
||||
];
|
||||
|
||||
$existing = \AKSRT\Settings\Settings::get_wechat_settings();
|
||||
if ($settings['app_secret'] === '***') {
|
||||
$settings['app_secret'] = $existing['app_secret'] ?? '';
|
||||
}
|
||||
|
||||
$merged = array_merge($existing, $settings);
|
||||
\AKSRT\Settings\Settings::update_wechat_settings($merged);
|
||||
delete_transient('aksrt_wechat_token');
|
||||
|
||||
return $this->success(['message' => __('微信设置已保存', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user