导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,420 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Bootstrap Loader
|
||||
*
|
||||
* 统一加载入口:辅助函数 → 类文件 → 实例初始化
|
||||
* 所有 require_once 路径集中管理,方便维护。
|
||||
*
|
||||
* @package AKSRT
|
||||
* @version 3.2.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// ============================================================
|
||||
// 翻译加载延迟到 after_setup_theme(WordPress 6.7+ 要求)
|
||||
// 由 AKSRT_Setup::load_textdomain() 处理
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// 阶段1:加载辅助函数(必须在类之前,类会调用这些函数)
|
||||
// ============================================================
|
||||
// 阶段1:加载辅助函数(必须在类之前,类会调用这些函数)
|
||||
// ============================================================
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-icons.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-domain-model.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-template-tags.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-frontend-tags.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-flow.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-homepage.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-slider.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-vip.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-i18n-switch.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-posts-renderer.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-resource.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-pay.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-credits.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/menu-decoration.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/class-aksrt-nav-walker.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-transactions.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-user-dashboard.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-shortcodes.php';
|
||||
|
||||
// ============================================================
|
||||
// 阶段2:加载类文件(settings 必须在 admin 之前)
|
||||
// ============================================================
|
||||
require_once AKSRT_DIR . '/inc/classes/setup-traits/trait-aksrt-setup-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/setup-traits/trait-aksrt-setup-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-setup.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/scripts-traits/trait-aksrt-scripts-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-scripts.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/frontend/class-aksrt-inline-style-builder.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/frontend/class-aksrt-custom-code-renderer.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/frontend/class-aksrt-loading-overlay-renderer.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-domain-groups.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-display-page-groups.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-credits-payment-groups.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-category-integration-groups.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/trait-aksrt-settings-auth-reader.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/trait-aksrt-settings-homepage-reader.php';
|
||||
|
||||
// Settings facade delegates(按领域分组加载,必须早于 admin)
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-mail.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-theme-search.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-post-display.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-home-cms.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-display-runtime.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-category-runtime.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-user-runtime.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-credits-runtime.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-integration.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-extension-runtime.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-forum-shop.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-capabilities.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-settings.php';
|
||||
// 加载后台控制器基类和业务域控制器(必须在 admin 之前)
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-controller-base.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-invite-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-report-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-verify-integrations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-restriction-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller-follow-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-product-admin-columns.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-order-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-fulfillment-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-coupon-gateway-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-variant-export-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/global-ctrl-traits/trait-aksrt-admin-global-controller-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-global-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-page-display-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-post-list-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-permissions-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-user-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-payment-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-tools-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-schema-provider.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-global.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-page-display.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-users.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-ip.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-audit.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-push.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-docs-mode.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-extensions.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools-import-export.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools-health.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools-migration.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools-backup.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools-email-rollback.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-tools.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-basic.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-list.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-contact.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-single.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-product-defaults.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-shipping-config.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop-timing.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-shop.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-basic.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-names.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-homepage.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-board-page.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-post-page.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-reply.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-permissions.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-other.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-upload.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum-credits.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/schema/class-aksrt-admin-schema-forum.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/import-export-traits/trait-aksrt-admin-import-export-controller-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-import-export-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-product-admin-columns.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-frontend-flow-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-order-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-fulfillment-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-coupon-gateway-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller-variant-export-operations.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-shop-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/forum-ctrl-traits/trait-aksrt-admin-forum-controller-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-forum-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-extension-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-comment-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-comment-actions.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-go-redirect.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-site-notice.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-api-audit.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-meilisearch.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-update-checker.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-category-rewrite.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-message-controller.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-settings-migration.php';
|
||||
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/auth/class-aksrt-auth-email-verification.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-auth-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/oauth-traits/trait-aksrt-oauth-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/oauth-traits/trait-aksrt-oauth-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-oauth.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-oauth-proxy-client.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-oauth-proxy-server.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-captcha.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/moderation-traits/trait-aksrt-moderation-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/moderation-traits/trait-aksrt-moderation-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-moderation.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-center.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-home.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-restriction.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-auth-router.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-templates.php';
|
||||
|
||||
// 加载访问量统计系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-stats-tracker.php';
|
||||
|
||||
// 邮箱回滚(14 天窗口)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-email-rollback.php';
|
||||
|
||||
// 原生用户菜单:积分管理
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
|
||||
|
||||
// 加载数据表迁移器(CMS / 积分 / 下载 token)
|
||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-3.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-db-migrator.php';
|
||||
|
||||
// 加载积分服务
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-credits.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-credits-earn.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-signin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-invite.php';
|
||||
|
||||
// 加载下载服务(CMS / 资源 token 流程)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-download.php';
|
||||
|
||||
// 下载状态统一解析器
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-download-state.php';
|
||||
|
||||
// 文档模式 Presenter(章节树、面包屑、章节导航)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-docs-presenter.php';
|
||||
|
||||
// 加载 CMS 首页模块系统
|
||||
|
||||
// 隐藏内容权限系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-content-visibility.php';
|
||||
|
||||
// 扩展服务层:IP 归属地、内容审核、搜索引擎推送
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-ip-geo.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-content-audit.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-search-push.php';
|
||||
|
||||
// 短信服务(签名助手 + Provider 接口 + Factory + 4 个 Adapter + 编排层)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-signature-helper.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider-aliyun.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider-tencent.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider-huawei.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider-custom.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms-provider-factory.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-sms.php';
|
||||
|
||||
// 微信通知骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-wechat-notify.php';
|
||||
|
||||
// 站内消息系统骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-message.php';
|
||||
|
||||
// 站内私信/会话系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-private-message.php';
|
||||
|
||||
// 举报治理骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-report-center.php';
|
||||
|
||||
// 关注系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-follow.php';
|
||||
|
||||
// 优惠券系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-coupon.php';
|
||||
|
||||
// 订单系统骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-order.php';
|
||||
|
||||
// 订单仓储(表存储 + 迁移兼容桥)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-order-repository.php';
|
||||
|
||||
// 支付网关接口与管理器
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-payment-gateway.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/gateways/class-aksrt-alipay-gateway.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/gateways/class-aksrt-wechatpay-gateway.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/gateways/class-aksrt-paypal-gateway.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/gateways/class-aksrt-stripe-gateway.php';
|
||||
|
||||
// 收益台账
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-revenue.php';
|
||||
|
||||
// 提现台账
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-withdrawal.php';
|
||||
|
||||
// 前台投稿系统骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-front-submit.php';
|
||||
|
||||
// 论坛系统骨架
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-permission-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-draft-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-comment-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-score-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/board-service-traits/trait-aksrt-forum-board-service-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/board-service-traits/trait-aksrt-forum-board-service-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/board-service-traits/trait-aksrt-forum-board-service-part-3.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-board-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/topic-service-traits/trait-aksrt-forum-topic-service-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/topic-service-traits/trait-aksrt-forum-topic-service-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/topic-service-traits/trait-aksrt-forum-topic-service-part-3.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/topic-service-traits/trait-aksrt-forum-topic-service-part-4.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-topic-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-moderation-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/view-builder-traits/trait-aksrt-forum-view-builder-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/view-builder-traits/trait-aksrt-forum-view-builder-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-forum-view-builder.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-forum.php';
|
||||
|
||||
// 商城系统骨架
|
||||
// 商城服务层
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-cart-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-checkout-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-address-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-product-service.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-3.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-shop.php';
|
||||
|
||||
// 售后服务
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-after-sale.php';
|
||||
|
||||
// 物流管理
|
||||
require_once AKSRT_DIR . '/inc/classes/shipping-traits/trait-aksrt-shipping-part-1.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/shipping-traits/trait-aksrt-shipping-part-2.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-shipping.php';
|
||||
|
||||
// 充值卡系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-charge-card.php';
|
||||
|
||||
// 返佣系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-rebate.php';
|
||||
|
||||
// AI 子系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-ai.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-ai-seo.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-ai-prompt.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-ai-controller.php';
|
||||
|
||||
// SEO head 单一装配器(站点/单篇 SEO、OG/Twitter、JSON-LD、canonical、robots)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-seo-head.php';
|
||||
|
||||
// 分类/标签 term meta(局部配置能力)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-term-meta.php';
|
||||
|
||||
// 商城 taxonomy term meta + 优惠活动 taxonomy 注册
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-shop-term-meta.php';
|
||||
|
||||
// 优惠计算引擎
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-discount.php';
|
||||
|
||||
// 商品评价/评分系统
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-product-review.php';
|
||||
|
||||
// 文章局部配置 meta box
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-post-meta-box.php';
|
||||
|
||||
// 资源 CPT 编辑器 meta box(仅后台用,前台无副作用)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-resource-meta-box.php';
|
||||
|
||||
// 加载原有小工具类(用户中心、分类目录、最新文章、标签云)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-widgets-original.php';
|
||||
|
||||
// 加载新增小工具(热榜文章、多栏目 Tab 切换)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-widgets-new.php';
|
||||
|
||||
// 加载第一批通用小工具(搜索、公告、最新评论、友链、图标卡片、按钮组、HTML间距)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-widgets-batch1.php';
|
||||
|
||||
// 自动加载 widgets 目录下的所有小工具文件
|
||||
$aksrt_widgets_dir = AKSRT_DIR . '/inc/classes/widgets/';
|
||||
if (is_dir($aksrt_widgets_dir)) {
|
||||
foreach (glob($aksrt_widgets_dir . 'class-*.php') as $aksrt_widget_file) {
|
||||
require_once $aksrt_widget_file;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载小工具注册中心
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-widgets.php';
|
||||
|
||||
// ============================================================
|
||||
// 阶段3:核心功能模块(Gravatar替换 / SMTP邮件)
|
||||
// ============================================================
|
||||
require_once AKSRT_DIR . '/inc/core-hooks.php';
|
||||
|
||||
// ============================================================
|
||||
// 阶段4:邮箱验证 — 启动 Auth Service 全局钩子(authenticate 拦截)
|
||||
// ============================================================
|
||||
if (class_exists('AKSRT_Auth_Service')) {
|
||||
AKSRT_Auth_Service::init();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 阶段5:邮箱验证 — 一次性 backfill(幂等)+ cron 异步回填
|
||||
// 兼容站点已通过 SQL 直接打开开关、未走 REST 保存路径的情况。
|
||||
// ============================================================
|
||||
add_action('init', function () {
|
||||
// 紧急降级:直接跳过迁移
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('AKSRT_Auth_Service') || !class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
if (!AKSRT_Settings::registration_requires_email_verification()) {
|
||||
return;
|
||||
}
|
||||
if (get_option('aksrt_email_verification_backfill_done')) {
|
||||
return;
|
||||
}
|
||||
// 在 admin 上下文下不要拖慢前台,仅 admin/cron 触发
|
||||
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) {
|
||||
return;
|
||||
}
|
||||
AKSRT_Auth_Service::backfill_legacy_users_as_verified();
|
||||
}, 5);
|
||||
|
||||
// 异步分批回填的 cron 钩子(仅在用户量 > 50000 时使用)
|
||||
add_action('aksrt_email_verification_backfill_batch', function ($offset = 0) {
|
||||
if (class_exists('AKSRT_Auth_Service')) {
|
||||
AKSRT_Auth_Service::run_async_backfill_batch((int) $offset);
|
||||
}
|
||||
}, 10, 1);
|
||||
|
||||
// ============================================================
|
||||
// 阶段6:设置迁移 — 将旧 option key 映射到新 Schema 结构
|
||||
// 仅在后台上下文执行,前台不触发。
|
||||
// ============================================================
|
||||
add_action('admin_init', function () {
|
||||
if (class_exists('AKSRT_Settings_Migration')) {
|
||||
AKSRT_Settings_Migration::get_instance()->maybe_migrate();
|
||||
}
|
||||
}, 5);
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — AI Controller
|
||||
*
|
||||
* Owns REST routes for the "AI 子系统" domain.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_AI_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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 {
|
||||
// AI status
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// AI settings update
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/settings', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// AI SEO suggest
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/seo-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'seo_suggest'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// AI summary suggest
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/summary-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'summary_suggest'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// AI tags suggest
|
||||
register_rest_route(self::API_NAMESPACE, '/ai/tags-suggest', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'tags_suggest'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /ai/status — AI 状态
|
||||
*/
|
||||
public function get_status(): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_AI')) {
|
||||
return $this->success(['data' => [
|
||||
'enabled' => false,
|
||||
'provider' => 'none',
|
||||
'configured' => false,
|
||||
'available' => false,
|
||||
]]);
|
||||
}
|
||||
|
||||
return $this->success(['data' => AKSRT_AI::get_instance()->get_status()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/settings — 更新 AI 设置
|
||||
*/
|
||||
public function update_settings(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_AI')) {
|
||||
return $this->error('AI 模块未加载', 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::get_instance()->update_config($config);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('AI 设置已保存', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('保存失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/seo-suggest — SEO 建议
|
||||
*/
|
||||
public function seo_suggest(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_AI_SEO')) {
|
||||
return $this->error('AI SEO 模块未加载', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
|
||||
if (empty($title)) {
|
||||
return $this->error('标题不能为空');
|
||||
}
|
||||
|
||||
$result = AKSRT_AI_SEO::get_instance()->suggest($title, $content);
|
||||
|
||||
if ($result['success']) {
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
return $this->error($result['message'] ?? '生成失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/summary-suggest — 摘要建议
|
||||
*/
|
||||
public function summary_suggest(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
|
||||
return $this->error('AI 模块未加载', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
$max_len = max(50, min(500, (int) ($params['max_length'] ?? 200)));
|
||||
|
||||
if (empty($content)) {
|
||||
return $this->error('内容不能为空');
|
||||
}
|
||||
|
||||
$prompt = AKSRT_AI_Prompt::get_instance()->build_summary_prompt($content, $max_len);
|
||||
$result = AKSRT_AI::get_instance()->generate($prompt);
|
||||
|
||||
if ($result['success']) {
|
||||
return $this->success(['data' => ['summary' => $result['content']]]);
|
||||
}
|
||||
|
||||
return $this->error($result['message'] ?? '生成失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ai/tags-suggest — 标签建议
|
||||
*/
|
||||
public function tags_suggest(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
|
||||
return $this->error('AI 模块未加载', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
|
||||
if (empty($title) && empty($content)) {
|
||||
return $this->error('标题或内容不能为空');
|
||||
}
|
||||
|
||||
$prompt = AKSRT_AI_Prompt::get_instance()->build_tags_prompt($title, $content);
|
||||
$result = AKSRT_AI::get_instance()->generate($prompt);
|
||||
|
||||
if ($result['success']) {
|
||||
$tags = json_decode($result['content'], true);
|
||||
if (!is_array($tags)) {
|
||||
$tags = array_filter(array_map('trim', explode(',', $result['content'])));
|
||||
}
|
||||
return $this->success(['data' => ['tags' => $tags]]);
|
||||
}
|
||||
|
||||
return $this->error($result['message'] ?? '生成失败');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin REST Controller — Comments
|
||||
*
|
||||
* Provides REST endpoints for comment management under aksrt/v1/comments.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Comment_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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
|
||||
/**
|
||||
* AKSRT Admin Controller Base
|
||||
*
|
||||
* 所有后台业务域控制器的基类。
|
||||
* 提供通用的权限检查、响应格式化、设置读写等基础设施。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
abstract class AKSRT_Admin_Controller_Base {
|
||||
|
||||
/**
|
||||
* REST API namespace (shared with AKSRT_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 '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 = AKSRT_Admin_Schema_Provider::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 = AKSRT_Admin_Schema_Provider::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,456 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Extension Controller
|
||||
*
|
||||
* Owns REST routes for the "扩展&增强" domain.
|
||||
* Aggregates real service status from SMS, WeChat, Captcha, IP Geo, Content Audit, Search Push.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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(AKSRT_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'],
|
||||
]);
|
||||
|
||||
// Docs page stats
|
||||
register_rest_route(self::API_NAMESPACE, '/docs/stats', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_docs_stats'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Docs search (public, for frontend AJAX)
|
||||
register_rest_route(self::API_NAMESPACE, '/docs/search', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'search_docs'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// 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 {
|
||||
$nav_count = 0;
|
||||
$special_count = 0;
|
||||
|
||||
$nav_query = new WP_Query([
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_wp_page_template',
|
||||
'meta_value' => 'templates/pages/page-documentnav.php',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'no_found_rows' => false,
|
||||
]);
|
||||
$nav_count = (int) $nav_query->found_posts;
|
||||
|
||||
$special_query = new WP_Query([
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_wp_page_template',
|
||||
'meta_value' => 'templates/pages/page-special.php',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'no_found_rows' => false,
|
||||
]);
|
||||
$special_count = (int) $special_query->found_posts;
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'total' => $nav_count + $special_count,
|
||||
'nav_count' => $nav_count,
|
||||
'special_count' => $special_count,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /docs/search — search document pages by keyword.
|
||||
*/
|
||||
public function search_docs(WP_REST_Request $request): WP_REST_Response {
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$root_id = (int) $request->get_param('root_id');
|
||||
$limit = max(1, min(50, (int) ($request->get_param('limit') ?: 20)));
|
||||
|
||||
if ($keyword === '') {
|
||||
return $this->success(['data' => ['results' => [], 'total' => 0]]);
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Docs_Presenter')) {
|
||||
return $this->success(['data' => ['results' => [], 'total' => 0]]);
|
||||
}
|
||||
|
||||
$results = AKSRT_Docs_Presenter::get_instance()->search_pages($keyword, $root_id, $limit);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'results' => $results,
|
||||
'total' => count($results),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] ?? '');
|
||||
}
|
||||
|
||||
// 保留旧布尔字段兼容
|
||||
$flat['sms_enabled'] = !empty($services['sms']['enabled']);
|
||||
$flat['wechat_enabled'] = !empty($services['wechat']['enabled']);
|
||||
$flat['captcha_enabled'] = !empty($services['captcha']['enabled']);
|
||||
$flat['baidu_push_enabled'] = !empty($services['push']['enabled']);
|
||||
$flat['content_audit_enabled'] = !empty($services['audit']['enabled']);
|
||||
|
||||
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::get_all();
|
||||
|
||||
$email_gate_enabled = true;
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
$email_gate_enabled = false;
|
||||
}
|
||||
|
||||
$captcha_comment_enabled = !empty($settings['captcha_comment_enabled']);
|
||||
$captcha_download_enabled = !empty($settings['captcha_download_enabled']);
|
||||
|
||||
$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_comment_enabled' => $captcha_comment_enabled,
|
||||
'captcha_download_enabled' => $captcha_download_enabled,
|
||||
'content_audit_enabled' => !empty($services['audit']['enabled']),
|
||||
'content_audit_configured' => !empty($services['audit']['configured']),
|
||||
'ip_geo_enabled' => !empty($services['ip_geo']['enabled']),
|
||||
'ip_geo_configured' => !empty($services['ip_geo']['configured']),
|
||||
'captcha_enabled' => $captcha_comment_enabled || $captcha_download_enabled,
|
||||
'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::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.
|
||||
*/
|
||||
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_IP_Geo')) {
|
||||
return $this->error(__('IP 归属地服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$result = AKSRT_IP_Geo::get_instance()->lookup($ip);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'ip' => $ip,
|
||||
'result' => $result,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /extensions/audit-test — test text content against current audit provider.
|
||||
*/
|
||||
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_Content_Audit')) {
|
||||
return $this->error(__('内容审核服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$result = AKSRT_Content_Audit::get_instance()->audit_text($text);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'text' => $text,
|
||||
'result' => $result,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有扩展服务的运行时状态
|
||||
*/
|
||||
private function collect_service_statuses(): array {
|
||||
$services = [];
|
||||
|
||||
// SMS
|
||||
if (class_exists('AKSRT_SMS')) {
|
||||
$services['sms'] = AKSRT_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_WeChat_Notify')) {
|
||||
$services['wechat'] = AKSRT_WeChat_Notify::get_instance()->get_status();
|
||||
} else {
|
||||
$services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Captcha
|
||||
$settings = AKSRT_Settings::get_all();
|
||||
$captcha_en = class_exists('AKSRT_Captcha') && !empty($settings['captcha_enabled']);
|
||||
$services['captcha'] = [
|
||||
'enabled' => $captcha_en,
|
||||
'provider' => sanitize_key((string) ($settings['captcha_provider'] ?? '')),
|
||||
'configured' => $captcha_en,
|
||||
'available' => $captcha_en,
|
||||
'verified' => false,
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => [],
|
||||
];
|
||||
|
||||
// IP Geo
|
||||
if (class_exists('AKSRT_IP_Geo')) {
|
||||
$services['ip_geo'] = AKSRT_IP_Geo::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_Content_Audit')) {
|
||||
$services['audit'] = AKSRT_Content_Audit::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_Search_Push')) {
|
||||
$services['push'] = AKSRT_Search_Push::get_instance()->get_status();
|
||||
} else {
|
||||
$services['push'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []];
|
||||
}
|
||||
|
||||
// Docs Mode
|
||||
$docs_en = !empty($settings['docs_mode_enabled']);
|
||||
$services['docs_mode'] = [
|
||||
'enabled' => $docs_en,
|
||||
'provider' => 'builtin',
|
||||
'configured' => $docs_en,
|
||||
'available' => $docs_en,
|
||||
'verified' => true,
|
||||
'transport' => 'builtin',
|
||||
'missing_fields' => [],
|
||||
];
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
/**
|
||||
* Forum REST controller.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Forum_Controller extends AKSRT_Admin_Controller_Base {
|
||||
use AKSRT_Admin_Forum_Controller_Part_1;
|
||||
use AKSRT_Admin_Forum_Controller_Part_2;
|
||||
|
||||
|
||||
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(AKSRT_Admin $admin): void {
|
||||
$this->admin = $admin;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/boards', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_boards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/boards', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_board'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_board'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_board'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/boards/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'reorder_boards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_topics'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_topic_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/moderate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'moderate_topic'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/vote', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'vote_topic'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/update-topic', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_topic_legacy'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_moderators'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_moderators'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Change moderator role
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderators/role', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'change_moderator_role'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderator/apply', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'apply_moderator'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_moderator_applications'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications/(?P<id>[A-Za-z0-9-]+)/review', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'review_moderator_application'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/favorite', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'toggle_favorite'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/score', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'score_topic'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/adopt', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'adopt_answer'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// §7.0 Plate CPT endpoints
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_plates'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_plate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_plate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_plate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_plate'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/reorder', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'reorder_plates'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// §7.2 Follow plate
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)/follow', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'toggle_follow_plate'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// §7.2 Plate categories (分区)
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plate-categories', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_plate_categories'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// §7.2 Sorted plates
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/plates/sorted', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_sorted_plates'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// §7.2 Front-end plate CRUD (forum admin/moderator)
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/front/plates', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'frontend_create_plate'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'frontend_update_plate'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'frontend_delete_plate'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_overview(): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => ['stats' => [], 'boards' => [], 'recent_topics' => [], 'pending_applications' => []]]);
|
||||
}
|
||||
|
||||
$pending_topics = $forum->get_topic_list([
|
||||
'limit' => 6,
|
||||
'status' => 'pending',
|
||||
'review_status' => AKSRT_Forum::REVIEW_PENDING,
|
||||
'viewer_user_id' => get_current_user_id(),
|
||||
'include_hidden' => true,
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'stats' => $forum->get_stats(),
|
||||
'boards' => array_slice($forum->get_boards(), 0, 6),
|
||||
'pending_topics' => $pending_topics['items'],
|
||||
'pending_applications' => array_slice($forum->get_moderator_application_list(['status' => AKSRT_Forum::APPLICATION_PENDING]), 0, 6),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_boards(): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
$items = $forum ? $forum->get_boards() : [];
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => count($items),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_board(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$name = sanitize_text_field((string) ($params['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
return $this->error(__('版块名称不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$term_id = $forum->create_board($params);
|
||||
if ($term_id <= 0) {
|
||||
return $this->error(__('创建版块失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('版块已创建', 'aksrt'),
|
||||
'data' => ['id' => $term_id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update_board(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$board_id = (int) $request->get_param('id');
|
||||
if ($board_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$success = $forum->update_board($board_id, $this->get_json_params($request));
|
||||
if (!$success) {
|
||||
return $this->error(__('版块更新失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('版块已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function delete_board(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$board_id = (int) $request->get_param('id');
|
||||
if ($board_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$success = $forum->delete_board($board_id);
|
||||
if (!$success) {
|
||||
return $this->error(__('版块删除失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('版块已删除', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function reorder_boards(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$order = $params['order'] ?? [];
|
||||
if (!is_array($order) || empty($order)) {
|
||||
return $this->error(__('排序参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$forum->reorder_boards($order);
|
||||
|
||||
return $this->success(['message' => __('排序已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §7.0 Plate CPT handlers
|
||||
// ================================================================
|
||||
|
||||
public function get_plates(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0]]);
|
||||
}
|
||||
|
||||
$args = [];
|
||||
$parent = $request->get_param('parent');
|
||||
if ($parent !== null) {
|
||||
$args['parent'] = (int) $parent;
|
||||
}
|
||||
$plate_cat = $request->get_param('plate_cat');
|
||||
if ($plate_cat) {
|
||||
$args['plate_cat'] = (int) $plate_cat;
|
||||
}
|
||||
|
||||
$items = $forum->get_plates($args);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => count($items),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
$plate = $forum->get_plate($plate_id);
|
||||
if (!$plate) {
|
||||
return $this->error(__('版块不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $plate]);
|
||||
}
|
||||
|
||||
public function create_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
$params = $this->get_json_params($request);
|
||||
$name = sanitize_text_field((string) ($params['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
return $this->error(__('版块名称不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$plate_id = $forum->create_plate($params);
|
||||
if ($plate_id <= 0) {
|
||||
return $this->error(__('创建版块失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('版块已创建', 'aksrt'),
|
||||
'data' => ['id' => $plate_id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Global & Functions Controller
|
||||
*
|
||||
* Handles REST routes for the "全局&功能" domain:
|
||||
* SEO, email/SMTP, footer, floating buttons, footer tabbar,
|
||||
* friend links, CMS features, homepage settings, category recommend.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Global_Controller extends AKSRT_Admin_Controller_Base {
|
||||
use AKSRT_Admin_Global_Controller_Part_1;
|
||||
use AKSRT_Admin_Global_Controller_Part_2;
|
||||
|
||||
|
||||
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::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::get_all();
|
||||
$settings['cms_features'] = array_merge(
|
||||
is_array($settings['cms_features'] ?? null) ? $settings['cms_features'] : [],
|
||||
$sanitized
|
||||
);
|
||||
update_option('aksrt_settings', $settings);
|
||||
AKSRT_Settings::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => 'CMS 功能已保存', 'cms_features' => AKSRT_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) {
|
||||
$args['cat'] = $term_id;
|
||||
}
|
||||
|
||||
$exclude_ids = class_exists('AKSRT_Settings')
|
||||
? AKSRT_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')
|
||||
? AKSRT_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 : 'list';
|
||||
$html = '';
|
||||
if (function_exists('aksrt_render_post_list')) {
|
||||
$html = aksrt_render_post_list($query, [
|
||||
'mode' => $mode,
|
||||
'context' => 'home',
|
||||
], false);
|
||||
} elseif ($query->have_posts()) {
|
||||
$html = '<div class="posts-row aksrt-homepage__posts" data-view="' . esc_attr($mode) . '">';
|
||||
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();
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
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 {
|
||||
$cats = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
|
||||
$data = [];
|
||||
foreach ($cats as $cat) {
|
||||
$data[] = ['id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug, 'count' => $cat->count, 'parent' => $cat->parent];
|
||||
}
|
||||
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' => 'default',
|
||||
'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' => in_array($params['layout_template'] ?? '', ['default', 'compact', 'centered'], true)
|
||||
? $params['layout_template'] : 'default',
|
||||
'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')
|
||||
? AKSRT_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')) {
|
||||
AKSRT_Settings::flush_settings_cache();
|
||||
}
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('Footer tabbar settings saved.', 'aksrt')]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Import/Export Controller
|
||||
*
|
||||
* Handles REST routes for settings export, import, section reset,
|
||||
* and health diagnostics.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Import_Export_Controller extends AKSRT_Admin_Controller_Base {
|
||||
use AKSRT_Admin_Import_Export_Controller_Part_1;
|
||||
use AKSRT_Admin_Import_Export_Controller_Part_2;
|
||||
|
||||
|
||||
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'],
|
||||
]);
|
||||
|
||||
// Migration status
|
||||
register_rest_route(self::API_NAMESPACE, '/settings/migration-status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_migration_status'],
|
||||
'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 = AKSRT_Admin_Schema_Provider::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::STANDALONE_OPTION_KEYS;
|
||||
|
||||
$export['standalone'] = [];
|
||||
foreach ($standalone_keys as $key) {
|
||||
$val = get_option($key);
|
||||
if ($val !== false) {
|
||||
$export['standalone'][$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
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 = AKSRT_Admin_Schema_Provider::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
|
||||
if (!empty($params['standalone']) && is_array($params['standalone'])) {
|
||||
foreach ($params['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::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 = AKSRT_Admin_Schema_Provider::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 = AKSRT_Admin_Schema_Provider::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::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',
|
||||
];
|
||||
|
||||
// Avatar source
|
||||
$avatar_source = AKSRT_Settings::get_all()['avatar_source'] ?? 'cn.cravatar.com';
|
||||
$checks['avatar_source'] = [
|
||||
'label' => '头像服务',
|
||||
'value' => $avatar_source,
|
||||
'status' => $avatar_source === 'cn.cravatar.com' ? '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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration status — reports schema version and pending migrations.
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin REST Controller — Messages & Conversations
|
||||
*
|
||||
* Provides REST endpoints for notifications and private messages
|
||||
* under aksrt/v1/messages and aksrt/v1/conversations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Message_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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_Message')) {
|
||||
return $this->success(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$msg = AKSRT_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_Message')) {
|
||||
return $this->success(['data' => ['unread' => 0]]);
|
||||
}
|
||||
$count = AKSRT_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_Message')) {
|
||||
return $this->error('not_available', '消息系统未启用。', 501);
|
||||
}
|
||||
$result = AKSRT_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_Message')) {
|
||||
return $this->error('not_available', '消息系统未启用。', 501);
|
||||
}
|
||||
$result = AKSRT_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_Private_Message')) {
|
||||
return $this->success(['data' => []]);
|
||||
}
|
||||
|
||||
$conversations = AKSRT_Private_Message::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_Private_Message')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$result = AKSRT_Private_Message::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_Private_Message')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$result = AKSRT_Private_Message::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_Private_Message')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
$conv_id = AKSRT_Private_Message::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_Private_Message')) {
|
||||
return $this->error('not_available', '私信系统未启用。', 501);
|
||||
}
|
||||
|
||||
AKSRT_Private_Message::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,201 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Page Display Controller
|
||||
*
|
||||
* Handles REST routes for the "页面&显示" domain:
|
||||
* Header components, banners, and homepage visual modules.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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(AKSRT_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',
|
||||
'enabled' => get_post_meta($post->ID, '_enabled', true) === '1',
|
||||
'show_text' => get_post_meta($post->ID, '_show_text', true) !== '0',
|
||||
'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([
|
||||
'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, '_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($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');
|
||||
$title = sanitize_text_field($params['title'] ?? '');
|
||||
if ($title === '') {
|
||||
return new WP_REST_Response(['code' => 'validation_error', 'message' => __('标题不能为空', 'aksrt')], 422);
|
||||
}
|
||||
|
||||
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', !empty($params['enabled']) ? '1' : '0');
|
||||
update_post_meta($id, '_show_text', !isset($params['show_text']) || !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,753 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Payment Controller
|
||||
*
|
||||
* Owns section-level REST routes for the "支付&付费" domain.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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')) {
|
||||
return $this->error('设置模块未初始化', 500);
|
||||
}
|
||||
|
||||
$paid = AKSRT_Settings::get_paid_content_settings();
|
||||
$recharge = AKSRT_Settings::get_payment_recharge_settings();
|
||||
$vip = AKSRT_Settings::get_payment_vip_settings();
|
||||
$revenue = AKSRT_Settings::get_payment_revenue_settings();
|
||||
$affiliate = AKSRT_Settings::get_payment_affiliate_settings();
|
||||
$gateway = AKSRT_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::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_Revenue')) {
|
||||
$revenue_stats = AKSRT_Revenue::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$withdrawal_stats = ['total' => 0, 'pending' => 0, 'completed' => 0, 'total_paid' => 0];
|
||||
if (class_exists('AKSRT_Withdrawal')) {
|
||||
$withdrawal_stats = AKSRT_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_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_Withdrawal::get_instance()->get_all([
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
]);
|
||||
|
||||
$total = AKSRT_Withdrawal::get_instance()->get_count(['status' => $status]);
|
||||
$stats = AKSRT_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_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_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_Charge_Card')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = AKSRT_DB_Migrator::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['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_Charge_Card::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_Charge_Card')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$code = sanitize_text_field((string) ($params['code'] ?? ''));
|
||||
$amount = (float) ($params['amount'] ?? 0);
|
||||
|
||||
if ($code === '' || $amount <= 0) {
|
||||
return $this->error('卡号和金额不能为空');
|
||||
}
|
||||
|
||||
$id = AKSRT_Charge_Card::get_instance()->create($params);
|
||||
|
||||
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_Charge_Card')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$amount = (float) ($params['amount'] ?? 0);
|
||||
$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'] ?? ''));
|
||||
|
||||
if ($amount <= 0) {
|
||||
return $this->error('金额必须大于 0');
|
||||
}
|
||||
|
||||
$service = AKSRT_Charge_Card::get_instance();
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = $prefix . strtoupper(wp_generate_password($length, false));
|
||||
$id = $service->create([
|
||||
'code' => $code,
|
||||
'amount' => $amount,
|
||||
'expires_at' => $expires,
|
||||
'note' => $note,
|
||||
]);
|
||||
if ($id > 0) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)),
|
||||
'data' => ['codes' => $codes, 'count' => count($codes)],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/charge-cards/{id}/disable — 禁用充值卡
|
||||
*/
|
||||
public function disable_charge_card(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Charge_Card')) {
|
||||
return $this->error('充值卡系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = AKSRT_Charge_Card::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_DB_Migrator::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_Rebate')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = AKSRT_DB_Migrator::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_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_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_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_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_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_DB_Migrator::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'));
|
||||
$statusMap = [
|
||||
'active' => __('未使用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
'expired' => __('已过期', 'aksrt'),
|
||||
'disabled' => __('已禁用', 'aksrt'),
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'code' => (string) ($row['code'] ?? ''),
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'balance' => (float) ($row['balance'] ?? 0),
|
||||
'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,171 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Permissions Controller
|
||||
*
|
||||
* Owns REST routes for the "功能&权限" domain:
|
||||
* Captcha, moderation, user permissions.
|
||||
*
|
||||
* Previously these routes were registered under the User Controller.
|
||||
* This controller provides proper domain separation.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Permissions_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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')) {
|
||||
return $this->error(__('能力矩阵模块不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$caps = AKSRT_Settings::get_user_capabilities();
|
||||
$defs = AKSRT_Settings::cap_definitions();
|
||||
$columns = AKSRT_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')) {
|
||||
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::cap_definitions();
|
||||
$columns = AKSRT_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::get_all();
|
||||
$all['user_caps'] = $clean;
|
||||
update_option('aksrt_settings', $all);
|
||||
AKSRT_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,24 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Post List Controller
|
||||
*
|
||||
* Owns section-level REST routes for the "文章&列表" domain.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Post_List_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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,384 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin Schema Provider
|
||||
*
|
||||
* Defines the canonical PHP-side settings contract:
|
||||
* domain -> section -> group -> field.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Provider {
|
||||
|
||||
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',
|
||||
'shop',
|
||||
'forum',
|
||||
];
|
||||
}
|
||||
|
||||
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',
|
||||
'shop' => 'aksrt_settings',
|
||||
'forum' => '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');
|
||||
if (!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');
|
||||
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 {
|
||||
return [
|
||||
'global' => 'AKSRT_Admin_Schema_Global',
|
||||
'page_display' => 'AKSRT_Admin_Schema_Page_Display',
|
||||
'post_list' => 'AKSRT_Admin_Schema_Post_List',
|
||||
'permissions' => 'AKSRT_Admin_Schema_Permissions',
|
||||
'users' => 'AKSRT_Admin_Schema_Users',
|
||||
'payment' => 'AKSRT_Admin_Schema_Payment',
|
||||
'extensions' => 'AKSRT_Admin_Schema_Extensions',
|
||||
'tools' => 'AKSRT_Admin_Schema_Tools',
|
||||
'shop' => 'AKSRT_Admin_Schema_Shop',
|
||||
'forum' => 'AKSRT_Admin_Schema_Forum',
|
||||
];
|
||||
}
|
||||
|
||||
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']];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$updated = update_option($option_key, $stored);
|
||||
AKSRT_Settings::flush_settings_cache();
|
||||
|
||||
if (isset($stored['site_favicon'])) {
|
||||
AKSRT_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) {
|
||||
foreach ($group['fields'] ?? [] as $field) {
|
||||
$fields[] = $field;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function sanitize_value($value, string $type) {
|
||||
switch ($type) {
|
||||
case 'html':
|
||||
return wp_kses_post((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':
|
||||
return is_array($value) ? $value : [];
|
||||
default:
|
||||
return sanitize_text_field((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
private function infer_sanitize_type(string $field_type): string {
|
||||
$map = [
|
||||
'switch' => 'bool',
|
||||
'input' => 'string',
|
||||
'text' => 'string',
|
||||
'textarea' => 'string',
|
||||
'number' => 'int',
|
||||
'slider' => 'int',
|
||||
'select' => 'string',
|
||||
'radio' => 'string',
|
||||
'checkbox' => 'array',
|
||||
'repeater' => 'array',
|
||||
'color' => 'string',
|
||||
'icon' => 'string',
|
||||
'media' => 'url',
|
||||
'code' => 'code',
|
||||
];
|
||||
|
||||
return $map[$field_type] ?? 'string';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop coupon and gateway operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Coupon_Gateway_Operations {
|
||||
|
||||
/**
|
||||
* GET /shop/coupons — 优惠券列表
|
||||
*/
|
||||
public function get_coupons(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Coupon')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$search = sanitize_text_field((string) $request->get_param('search'));
|
||||
|
||||
$result = AKSRT_Coupon::get_instance()->get_list([
|
||||
'status' => $status,
|
||||
'search' => $search,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
]);
|
||||
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/coupons — 创建优惠券
|
||||
*/
|
||||
public function create_coupon(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Coupon')) {
|
||||
return $this->error('优惠券系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$code = sanitize_text_field((string) ($params['code'] ?? ''));
|
||||
|
||||
if ($code === '') {
|
||||
return $this->error('优惠码不能为空');
|
||||
}
|
||||
|
||||
$id = AKSRT_Coupon::get_instance()->create($params);
|
||||
|
||||
if ($id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('优惠券已创建', 'aksrt'),
|
||||
'data' => ['id' => $id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return $this->error(__('创建失败(可能优惠码已存在)', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/coupons/{id}/disable — 禁用优惠券
|
||||
*/
|
||||
public function disable_coupon(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Coupon')) {
|
||||
return $this->error('优惠券系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = AKSRT_Coupon::get_instance()->disable($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('优惠券已禁用', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('操作失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /shop/gateways — 支付网关状态列表
|
||||
*/
|
||||
public function get_gateways(): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Payment_Gateway')) {
|
||||
return $this->success(['data' => ['gateways' => []]]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'gateways' => AKSRT_Payment_Gateway::get_instance()->get_status_summary(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop frontend flow operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations {
|
||||
|
||||
public function check_logged_in_permission(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
public function preview_checkout(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->preview_checkout(get_current_user_id(), $this->get_json_params($request));
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '结算预览失败');
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function create_frontend_order(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->create_checkout_order(get_current_user_id(), $this->get_json_params($request));
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '订单创建失败');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('订单已创建', 'aksrt'),
|
||||
'data' => $result['data'],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function get_frontend_orders(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
|
||||
$view = AKSRT_Shop::get_instance()->get_order_center_view(get_current_user_id(), [
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
return $this->success(['data' => $view]);
|
||||
}
|
||||
|
||||
public function get_frontend_order(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$order = AKSRT_Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
|
||||
if (!$order) {
|
||||
return $this->error('订单不存在', 404);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $order]);
|
||||
}
|
||||
|
||||
public function pay_frontend_order(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '支付失败');
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function cancel_frontend_order(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$result = AKSRT_Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '取消订单失败');
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function create_frontend_after_sale(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$params['order_number'] = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$result = AKSRT_Shop::get_instance()->create_after_sale_request(get_current_user_id(), $params);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '提交售后失败');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('售后申请已提交', 'aksrt'),
|
||||
'data' => $result['data'],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function get_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
$shop = AKSRT_Shop::get_instance();
|
||||
$cart = $shop->get_cart($user_id);
|
||||
|
||||
$items = [];
|
||||
foreach ($cart as $pid => $opts_map) {
|
||||
$product = $shop->build_product_card_view((int) $pid);
|
||||
if (empty($product) || !is_array($opts_map)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($opts_map as $opt_str => $qty) {
|
||||
$items[] = [
|
||||
'product_id' => (int) $pid,
|
||||
'options_string' => $opt_str === '_default' ? '' : $opt_str,
|
||||
'quantity' => (int) $qty,
|
||||
'product' => $product,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'count' => $shop->get_cart_count($user_id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_cart_count(WP_REST_Request $request): WP_REST_Response {
|
||||
$user_id = get_current_user_id();
|
||||
return $this->success(['data' => ['count' => AKSRT_Shop::get_instance()->get_cart_count($user_id)]]);
|
||||
}
|
||||
|
||||
public function handle_add_to_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$product_id = (int) ($params['product_id'] ?? 0);
|
||||
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
|
||||
$count = max(1, (int) ($params['count'] ?? 1));
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->add_to_cart(get_current_user_id(), $product_id, $options_string, $count);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $this->error($result['error']);
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function handle_reduce_from_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$product_id = (int) ($params['product_id'] ?? 0);
|
||||
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
|
||||
$count = max(1, (int) ($params['count'] ?? 1));
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->reduce_from_cart(get_current_user_id(), $product_id, $options_string, $count);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $this->error($result['error']);
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function handle_remove_from_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$product_id = (int) ($params['product_id'] ?? 0);
|
||||
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->remove_from_cart(get_current_user_id(), $product_id, $options_string);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $this->error($result['error']);
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function handle_remove_multi_from_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$items = is_array($params['items'] ?? null) ? $params['items'] : [];
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->remove_multi_from_cart(get_current_user_id(), $items);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $this->error($result['error']);
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function handle_empty_cart(WP_REST_Request $request): WP_REST_Response {
|
||||
$result = AKSRT_Shop::get_instance()->empty_cart(get_current_user_id());
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return $this->error($result['error']);
|
||||
}
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
public function get_frontend_addresses(): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->success(['data' => ['addresses' => []]]);
|
||||
}
|
||||
|
||||
$addresses = AKSRT_Shop::get_instance()->get_address_book(get_current_user_id());
|
||||
return $this->success(['data' => ['addresses' => $addresses]]);
|
||||
}
|
||||
|
||||
public function create_frontend_address(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$user_id = get_current_user_id();
|
||||
$result = AKSRT_Shop::get_instance()->add_address($user_id, $params);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '添加地址失败');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('地址已添加', 'aksrt'),
|
||||
'data' => ['address' => $result['address']],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update_frontend_address(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$address_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$user_id = get_current_user_id();
|
||||
$result = AKSRT_Shop::get_instance()->update_address($user_id, $address_id, $params);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '更新地址失败');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('地址已更新', 'aksrt'),
|
||||
'data' => ['address' => $result['address']],
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete_frontend_address(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$address_id = (int) $request->get_param('id');
|
||||
$user_id = get_current_user_id();
|
||||
$result = AKSRT_Shop::get_instance()->delete_address($user_id, $address_id);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '删除地址失败');
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('地址已删除', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function set_default_frontend_address(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$address_id = (int) $request->get_param('id');
|
||||
$user_id = get_current_user_id();
|
||||
$result = AKSRT_Shop::get_instance()->set_default_address($user_id, $address_id);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '设置默认地址失败');
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('已设为默认地址', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function create_frontend_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$params['author_id'] = get_current_user_id();
|
||||
$frontend_request = new WP_REST_Request('POST', '/');
|
||||
$frontend_request->set_body(wp_json_encode($params));
|
||||
|
||||
return $this->create_product($frontend_request);
|
||||
}
|
||||
|
||||
public function update_frontend_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'aksrt_product') {
|
||||
return $this->error('商品不存在', 404);
|
||||
}
|
||||
|
||||
if ((int) $post->post_author !== get_current_user_id() && !current_user_can('manage_options')) {
|
||||
return $this->error('没有权限编辑该商品', 403);
|
||||
}
|
||||
|
||||
return $this->update_product($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop fulfillment operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Fulfillment_Operations {
|
||||
|
||||
/**
|
||||
* GET /shop/after-sale — 售后工单列表
|
||||
*/
|
||||
public function get_after_sale_list(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_After_Sale')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$stage = sanitize_key((string) $request->get_param('stage'));
|
||||
$order = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$type = sanitize_key((string) $request->get_param('type'));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$result = AKSRT_After_Sale::get_instance()->get_list([
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
'status' => $status,
|
||||
'stage' => $stage,
|
||||
'order_number' => $order,
|
||||
'type' => $type,
|
||||
'date_from' => $date_from,
|
||||
'date_to' => $date_to,
|
||||
'include_order_context' => true,
|
||||
]);
|
||||
|
||||
$stats = AKSRT_After_Sale::get_instance()->get_stats();
|
||||
|
||||
return $this->success(['data' => array_merge($result, [
|
||||
'stats' => $stats,
|
||||
'filters' => [
|
||||
'status' => $status,
|
||||
'stage' => $stage,
|
||||
'order_number' => $order,
|
||||
'type' => $type,
|
||||
'date_from' => $date_from,
|
||||
'date_to' => $date_to,
|
||||
],
|
||||
'options' => [
|
||||
'status' => AKSRT_After_Sale::get_status_options(),
|
||||
'stage' => AKSRT_After_Sale::get_stage_options(),
|
||||
'type' => AKSRT_After_Sale::get_type_options(),
|
||||
],
|
||||
])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/after-sale — 创建售后工单
|
||||
*/
|
||||
public function create_after_sale(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_After_Sale')) {
|
||||
return $this->error('售后系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
|
||||
if ($order_number === '' || $reason === '') {
|
||||
return $this->error('订单号和原因不能为空');
|
||||
}
|
||||
|
||||
$order = class_exists('AKSRT_Order_Repository')
|
||||
? AKSRT_Order_Repository::get_instance()->get($order_number)
|
||||
: null;
|
||||
|
||||
if (!$order) {
|
||||
return $this->error(__('订单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$params['user_id'] = (int) ($params['user_id'] ?? ($order['user_id'] ?? 0));
|
||||
if ($params['user_id'] <= 0) {
|
||||
return $this->error(__('订单未绑定有效用户', 'aksrt'));
|
||||
}
|
||||
|
||||
$id = AKSRT_After_Sale::get_instance()->create($params);
|
||||
|
||||
if ($id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('售后工单已创建', 'aksrt'),
|
||||
'data' => ['id' => $id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return $this->error(__('创建失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/after-sale/{id}/update-status — 更新售后工单状态
|
||||
*/
|
||||
public function update_after_sale_status(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_After_Sale')) {
|
||||
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['operator_note'] ?? ''));
|
||||
|
||||
if ($id <= 0 || $new_status === '') {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = AKSRT_After_Sale::get_instance()->update_status(
|
||||
$id,
|
||||
$new_status,
|
||||
get_current_user_id(),
|
||||
$note
|
||||
);
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('状态已更新', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /shop/shipping — 发货记录列表
|
||||
*/
|
||||
public function get_shipping_list(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shipping')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$order = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
$tracking_no = sanitize_text_field((string) $request->get_param('tracking_no'));
|
||||
|
||||
$result = AKSRT_Shipping::get_instance()->get_list([
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
'status' => $status,
|
||||
'order_number' => $order,
|
||||
'date_from' => $date_from,
|
||||
'date_to' => $date_to,
|
||||
'tracking_no' => $tracking_no,
|
||||
'include_order_context' => true,
|
||||
]);
|
||||
|
||||
$stats = AKSRT_Shipping::get_instance()->get_stats();
|
||||
|
||||
return $this->success(['data' => array_merge($result, [
|
||||
'stats' => $stats,
|
||||
'filters' => [
|
||||
'status' => $status,
|
||||
'order_number' => $order,
|
||||
'date_from' => $date_from,
|
||||
'date_to' => $date_to,
|
||||
'tracking_no' => $tracking_no,
|
||||
],
|
||||
'options' => [
|
||||
'status' => AKSRT_Shipping::get_status_options(),
|
||||
'carrier' => AKSRT_Shipping::get_carrier_options(),
|
||||
],
|
||||
])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/shipping — 创建发货记录
|
||||
*/
|
||||
public function create_shipping(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shipping')) {
|
||||
return $this->error('物流系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
|
||||
$carrier = sanitize_text_field((string) ($params['carrier'] ?? ''));
|
||||
$tracking_no = sanitize_text_field((string) ($params['tracking_no'] ?? ''));
|
||||
|
||||
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
|
||||
return $this->error('订单号、快递公司和物流单号不能为空');
|
||||
}
|
||||
|
||||
$order = class_exists('AKSRT_Order_Repository')
|
||||
? AKSRT_Order_Repository::get_instance()->get($order_number)
|
||||
: null;
|
||||
|
||||
if (!$order) {
|
||||
return $this->error(__('订单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$params['operator_id'] = get_current_user_id();
|
||||
|
||||
$id = AKSRT_Shipping::get_instance()->create($params);
|
||||
|
||||
if ($id > 0) {
|
||||
return $this->success([
|
||||
'message' => __('发货记录已创建', 'aksrt'),
|
||||
'data' => ['id' => $id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
return $this->error(__('创建失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/shipping/{id}/update-status — 更新发货状态
|
||||
*/
|
||||
public function update_shipping_status(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shipping')) {
|
||||
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'] ?? ''));
|
||||
|
||||
if ($id <= 0 || $new_status === '') {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = AKSRT_Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('状态已更新', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
|
||||
public function get_after_sale_detail(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_After_Sale')) {
|
||||
return $this->error('售后系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$record = AKSRT_After_Sale::get_instance()->get_with_order_context($id);
|
||||
if (!$record) {
|
||||
return $this->error(__('售后工单不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'record' => $record,
|
||||
'timeline' => $this->build_after_sale_timeline($record),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_shipping_detail(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shipping')) {
|
||||
return $this->error('物流系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$record = AKSRT_Shipping::get_instance()->get_with_order_context($id);
|
||||
if (!$record) {
|
||||
return $this->error(__('发货记录不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'record' => $record,
|
||||
'timeline' => $this->build_shipping_timeline($record),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function batch_update_shipping_status(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shipping')) {
|
||||
return $this->error('物流系统未启用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$ids = is_array($params['ids'] ?? null) ? $params['ids'] : [];
|
||||
$status = sanitize_key((string) ($params['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
|
||||
if (empty($ids) || $status === '') {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$result = AKSRT_Shipping::get_instance()->batch_update_status($ids, $status, $note, get_current_user_id());
|
||||
|
||||
return $this->success([
|
||||
'message' => __('批量状态更新已执行', 'aksrt'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_shipping_timeline(array $record): array {
|
||||
$timeline = [];
|
||||
|
||||
foreach ([
|
||||
'created_at' => __('创建发货记录', 'aksrt'),
|
||||
'updated_at' => __('更新物流状态', 'aksrt'),
|
||||
'delivered_at' => __('确认签收', 'aksrt'),
|
||||
] as $field => $label) {
|
||||
if (!empty($record[$field])) {
|
||||
$timeline[] = [
|
||||
'time' => (string) $record[$field],
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
usort($timeline, static function ($a, $b) {
|
||||
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
|
||||
});
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
private function build_after_sale_timeline(array $record): array {
|
||||
$timeline = [];
|
||||
|
||||
foreach ([
|
||||
'created_at' => __('提交售后申请', 'aksrt'),
|
||||
'updated_at' => __('更新售后状态', 'aksrt'),
|
||||
'resolved_at' => __('售后处理结束', 'aksrt'),
|
||||
] as $field => $label) {
|
||||
if (!empty($record[$field])) {
|
||||
$timeline[] = [
|
||||
'time' => (string) $record[$field],
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
usort($timeline, static function ($a, $b) {
|
||||
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
|
||||
});
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop order operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Order_Operations {
|
||||
|
||||
/**
|
||||
* GET /shop/orders — 订单列表,支持 search、status、page、per_page
|
||||
*/
|
||||
public function get_orders(WP_REST_Request $request): WP_REST_Response {
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$search = sanitize_text_field((string) $request->get_param('search'));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
|
||||
if (class_exists('AKSRT_Order_Repository')) {
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
|
||||
$args = [
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
];
|
||||
if ($status !== '' && $status !== 'all') {
|
||||
$args['status'] = $status;
|
||||
}
|
||||
|
||||
$orders = $repo->get_orders_admin($args, $search);
|
||||
$total = $repo->get_orders_count($args, $search);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $orders,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error('订单仓库不可用', 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /shop/orders/{order_number} — 单个订单详情(含上下文)
|
||||
*/
|
||||
public function get_order(WP_REST_Request $request): WP_REST_Response {
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
|
||||
if (!class_exists('AKSRT_Order_Repository')) {
|
||||
return $this->error('订单服务不可用', 500);
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$order = $repo->get($order_number);
|
||||
if (!$order) {
|
||||
return $this->error('订单不存在', 404);
|
||||
}
|
||||
|
||||
$context = $repo->get_admin_order_context($order_number);
|
||||
$timeline = [];
|
||||
if (class_exists('AKSRT_Shop')) {
|
||||
$shipping_records = class_exists('AKSRT_Shipping') ? AKSRT_Shipping::get_instance()->get_by_order($order_number) : [];
|
||||
$after_sale_result = class_exists('AKSRT_After_Sale') ? AKSRT_After_Sale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20]) : ['items' => []];
|
||||
$after_sale_items = $after_sale_result['items'] ?? [];
|
||||
|
||||
$timeline = [];
|
||||
$date_fields = [
|
||||
'created_at' => '订单创建',
|
||||
'paid_at' => '完成支付',
|
||||
];
|
||||
foreach ($date_fields as $field => $label) {
|
||||
if (!empty($order[$field])) {
|
||||
$timeline[] = ['time' => $order[$field], 'title' => $label, 'type' => 'order'];
|
||||
}
|
||||
}
|
||||
foreach ($shipping_records as $record) {
|
||||
$timeline[] = [
|
||||
'time' => $record['created_at'] ?? '',
|
||||
'title' => '物流更新:' . ($record['status_label'] ?? $record['status'] ?? ''),
|
||||
'detail' => ($record['carrier_label'] ?? '') . ' / ' . ($record['tracking_no'] ?? ''),
|
||||
'type' => 'shipping',
|
||||
];
|
||||
}
|
||||
foreach ($after_sale_items as $record) {
|
||||
$timeline[] = [
|
||||
'time' => $record['created_at'] ?? '',
|
||||
'title' => '售后:' . ($record['type_label'] ?? '') . ' — ' . ($record['status_label'] ?? ''),
|
||||
'detail' => $record['reason'] ?? '',
|
||||
'type' => 'after_sale',
|
||||
];
|
||||
}
|
||||
usort($timeline, static function ($a, $b) {
|
||||
return strcmp((string) ($b['time'] ?? ''), (string) ($a['time'] ?? ''));
|
||||
});
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => $order,
|
||||
'context' => $context,
|
||||
'timeline' => $timeline,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/orders/update-status — 更新订单状态
|
||||
* Canonical field: order_number (前端必须发送 order_number)
|
||||
*/
|
||||
public function update_order_status(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
|
||||
$new_status = sanitize_key((string) ($params['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
|
||||
if ($order_number === '' || $new_status === '') {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = false;
|
||||
if (class_exists('AKSRT_Order_Repository')) {
|
||||
$success = AKSRT_Order_Repository::get_instance()->update_status($order_number, $new_status, [
|
||||
'note' => $note,
|
||||
'force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
return $this->success(['message' => __('订单状态已更新', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop product admin list operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Product_Admin_Columns {
|
||||
|
||||
/**
|
||||
* Register custom columns for the aksrt_product admin list table.
|
||||
*/
|
||||
public function init_product_admin_columns(): void {
|
||||
add_filter('manage_edit-aksrt_product_columns', [$this, 'product_columns']);
|
||||
add_action('manage_aksrt_product_posts_custom_column', [$this, 'render_product_column'], 10, 2);
|
||||
add_filter('manage_edit-aksrt_product_sortable_columns', [$this, 'sortable_product_columns']);
|
||||
add_action('quick_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
|
||||
add_action('bulk_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
|
||||
add_action('save_post_aksrt_product', [$this, 'save_product_quick_edit'], 10, 2);
|
||||
add_action('admin_footer-edit.php', [$this, 'product_quick_edit_script']);
|
||||
}
|
||||
|
||||
public function product_columns(array $columns): array {
|
||||
$new = [];
|
||||
foreach ($columns as $key => $label) {
|
||||
$new[$key] = $label;
|
||||
if ($key === 'title') {
|
||||
$new['product_price'] = __('价格', 'aksrt');
|
||||
$new['product_stock'] = __('库存', 'aksrt');
|
||||
$new['product_sales'] = __('销量', 'aksrt');
|
||||
$new['product_stats'] = __('综合数据', 'aksrt');
|
||||
$new['product_author_name'] = __('商家', 'aksrt');
|
||||
$new['product_discount'] = __('优惠活动', 'aksrt');
|
||||
}
|
||||
}
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function render_product_column(string $column, int $post_id): void {
|
||||
switch ($column) {
|
||||
case 'product_price':
|
||||
$price = get_post_meta($post_id, '_aksrt_price', true);
|
||||
echo esc_html(number_format_i18n((float) $price, 2));
|
||||
break;
|
||||
case 'product_stock':
|
||||
$stock = get_post_meta($post_id, '_aksrt_stock', true);
|
||||
$stock_type = get_post_meta($post_id, '_aksrt_stock_type', true);
|
||||
if ($stock_type === 'opts') {
|
||||
echo esc_html__('按选项', 'aksrt');
|
||||
} elseif ((int) $stock === -1) {
|
||||
echo esc_html__('无限', 'aksrt');
|
||||
} else {
|
||||
echo esc_html((string) (int) $stock);
|
||||
}
|
||||
break;
|
||||
case 'product_sales':
|
||||
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_sales', true));
|
||||
break;
|
||||
case 'product_stats':
|
||||
$views = (int) get_post_meta($post_id, '_aksrt_views', true);
|
||||
$fav = (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
|
||||
printf(
|
||||
esc_html__('阅读 %1$d / 收藏 %2$d', 'aksrt'),
|
||||
$views,
|
||||
$fav
|
||||
);
|
||||
break;
|
||||
case 'product_author_name':
|
||||
$post = get_post($post_id);
|
||||
if ($post) {
|
||||
$author = get_userdata($post->post_author);
|
||||
echo esc_html($author ? $author->display_name : '—');
|
||||
}
|
||||
break;
|
||||
case 'product_discount':
|
||||
$terms = wp_get_object_terms($post_id, 'shop_discount', ['fields' => 'all']);
|
||||
if (!is_wp_error($terms) && !empty($terms)) {
|
||||
$term_ids = wp_list_pluck($terms, 'term_id');
|
||||
$names = wp_list_pluck($terms, 'name');
|
||||
echo '<span class="aksrt-col-data" data-discount-id="' . esc_attr((string) ($term_ids[0] ?? 0)) . '"></span>';
|
||||
echo esc_html(implode(', ', $names));
|
||||
} else {
|
||||
echo '<span class="aksrt-col-data" data-discount-id="0"></span>—';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function sortable_product_columns(array $columns): array {
|
||||
$columns['product_sales'] = 'product_sales';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_product_quick_edit(string $column_name, string $post_type): void {
|
||||
if ($column_name !== 'product_discount' || $post_type !== 'aksrt_product') {
|
||||
return;
|
||||
}
|
||||
wp_nonce_field('aksrt_product_quick_edit', 'aksrt_product_qe_nonce');
|
||||
$discounts = get_terms([
|
||||
'taxonomy' => 'shop_discount',
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
?>
|
||||
<fieldset class="inline-edit-col-right" id="aksrt-product-quick-edit">
|
||||
<div class="inline-edit-col">
|
||||
<h4><?php esc_html_e('商品优惠活动', 'aksrt'); ?></h4>
|
||||
<input type="hidden" name="aksrt_product_qe_flag" value="1">
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('优惠活动', 'aksrt'); ?></span>
|
||||
<select name="aksrt_qe_discount" class="aksrt-qe-discount">
|
||||
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
|
||||
<option value="0"><?php esc_html_e('无优惠', 'aksrt'); ?></option>
|
||||
<?php if (!is_wp_error($discounts)) : ?>
|
||||
<?php foreach ($discounts as $term) : ?>
|
||||
<option value="<?php echo esc_attr($term->term_id); ?>"><?php echo esc_html($term->name); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function save_product_quick_edit(int $post_id, \WP_Post $post): void {
|
||||
if (!isset($_POST['aksrt_product_qe_nonce']) || !wp_verify_nonce($_POST['aksrt_product_qe_nonce'], 'aksrt_product_quick_edit')) {
|
||||
return;
|
||||
}
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_product_qe_flag'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discount_id = isset($_POST['aksrt_qe_discount']) ? sanitize_text_field(wp_unslash($_POST['aksrt_qe_discount'])) : '-1';
|
||||
if ($discount_id === '-1') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($discount_id === '0') {
|
||||
wp_set_object_terms($post_id, [], 'shop_discount');
|
||||
} else {
|
||||
$term_id = (int) $discount_id;
|
||||
if ($term_id > 0 && term_exists($term_id, 'shop_discount')) {
|
||||
wp_set_object_terms($post_id, [$term_id], 'shop_discount');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function product_quick_edit_script(): void {
|
||||
$screen = get_current_screen();
|
||||
if (!$screen || $screen->post_type !== 'aksrt_product') {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<script>
|
||||
(function(){
|
||||
var wpInlineEdit = window.inlineEditPost ? window.inlineEditPost.edit : null;
|
||||
if (!wpInlineEdit) return;
|
||||
window.inlineEditPost.edit = function(id) {
|
||||
wpInlineEdit.apply(this, arguments);
|
||||
var postId = 0;
|
||||
if (typeof id === 'object') {
|
||||
postId = parseInt(this.getId(id));
|
||||
}
|
||||
if (!postId) return;
|
||||
var row = document.getElementById('post-' + postId);
|
||||
if (!row) return;
|
||||
var dataEl = row.querySelector('.column-product_discount .aksrt-col-data');
|
||||
if (!dataEl) return;
|
||||
var termId = dataEl.getAttribute('data-discount-id') || '0';
|
||||
var select = document.querySelector('.aksrt-qe-discount');
|
||||
if (select) {
|
||||
select.value = termId;
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop product operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Product_Operations {
|
||||
|
||||
public function get_overview(): WP_REST_Response {
|
||||
$product_count = 0;
|
||||
|
||||
$post_type_obj = get_post_type_object('aksrt_product');
|
||||
if ($post_type_obj) {
|
||||
$counts = wp_count_posts('aksrt_product');
|
||||
$product_count = (int) ($counts->publish ?? 0);
|
||||
}
|
||||
|
||||
$order_stats = ['total' => 0, 'pending' => 0, 'paid' => 0, 'revenue' => 0];
|
||||
if (class_exists('AKSRT_Order_Repository')) {
|
||||
$order_stats = AKSRT_Order_Repository::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0];
|
||||
if (class_exists('AKSRT_Revenue')) {
|
||||
$revenue_stats = AKSRT_Revenue::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$gateway_summary = [];
|
||||
if (class_exists('AKSRT_Payment_Gateway')) {
|
||||
$gateway_summary = AKSRT_Payment_Gateway::get_instance()->get_status_summary();
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'enabled' => true,
|
||||
'product_count' => $product_count,
|
||||
'order_count' => $order_stats['total'],
|
||||
'order_pending' => $order_stats['pending'] ?? 0,
|
||||
'revenue_total' => $order_stats['revenue'] ?? 0,
|
||||
'revenue_pending' => $revenue_stats['pending'] ?? 0,
|
||||
'gateways' => $gateway_summary,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /shop/products — 商品列表,支持 search、status、page、per_page
|
||||
*/
|
||||
public function get_products(WP_REST_Request $request): WP_REST_Response {
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
|
||||
$search = sanitize_text_field((string) $request->get_param('search'));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
|
||||
$query_args = [
|
||||
'post_type' => 'aksrt_product',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $page,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
if ($status !== '') {
|
||||
$query_args['post_status'] = $status;
|
||||
} else {
|
||||
$query_args['post_status'] = ['publish', 'draft', 'pending'];
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$query_args['s'] = $search;
|
||||
}
|
||||
|
||||
$query = new WP_Query($query_args);
|
||||
|
||||
$products = [];
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$id = get_the_ID();
|
||||
$products[] = [
|
||||
'id' => $id,
|
||||
'title' => get_the_title(),
|
||||
'price' => (float) get_post_meta($id, '_aksrt_price', true),
|
||||
'image' => get_the_post_thumbnail_url($id, 'medium') ?: '',
|
||||
'stock' => (int) get_post_meta($id, '_aksrt_stock', true),
|
||||
'sales' => (int) get_post_meta($id, '_aksrt_sales', true),
|
||||
'status' => get_post_status(),
|
||||
'created' => get_the_date('Y-m-d H:i'),
|
||||
];
|
||||
}
|
||||
wp_reset_postdata();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $products,
|
||||
'total' => (int) $query->found_posts,
|
||||
'total_pages' => (int) $query->max_num_pages,
|
||||
'page' => $page,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /shop/products/{id} — 单个商品详情
|
||||
*/
|
||||
public function get_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$post = get_post($id);
|
||||
|
||||
if (!$post || $post->post_type !== 'aksrt_product') {
|
||||
return $this->error('商品不存在', 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'content' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'price' => (float) get_post_meta($post->ID, '_aksrt_price', true),
|
||||
'image' => (string) (get_post_meta($post->ID, '_aksrt_product_image', true) ?: get_the_post_thumbnail_url($post->ID, 'medium') ?: ''),
|
||||
'stock' => (int) get_post_meta($post->ID, '_aksrt_stock', true),
|
||||
'sales' => (int) get_post_meta($post->ID, '_aksrt_sales', true),
|
||||
'status' => $post->post_status,
|
||||
'created' => get_the_date('Y-m-d H:i', $post),
|
||||
'modified' => get_the_modified_date('Y-m-d H:i', $post),
|
||||
'categories' => wp_get_post_terms($post->ID, 'aksrt_product_cat', ['fields' => 'names']) ?: [],
|
||||
'tags' => wp_get_post_terms($post->ID, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
|
||||
'gallery' => get_post_meta($post->ID, '_aksrt_product_gallery', true) ?: [],
|
||||
'shipping_mode' => (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true),
|
||||
'shipping_fee' => (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true),
|
||||
'after_sale_policy' => (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true),
|
||||
'support_refund' => get_post_meta($post->ID, '_aksrt_support_refund', true) !== '0',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/products — 创建商品
|
||||
*/
|
||||
public function create_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
|
||||
$title = sanitize_text_field((string) ($params['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($params['content'] ?? ''));
|
||||
$excerpt = sanitize_textarea_field((string) ($params['excerpt'] ?? ''));
|
||||
$price = (float) ($params['price'] ?? 0);
|
||||
$stock = (int) ($params['stock'] ?? 0);
|
||||
$status = sanitize_key((string) ($params['status'] ?? 'publish'));
|
||||
$image = esc_url_raw((string) ($params['image'] ?? ''));
|
||||
$gallery = is_array($params['gallery'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['gallery']))) : [];
|
||||
$shipping_mode = sanitize_key((string) ($params['shipping_mode'] ?? 'express'));
|
||||
$shipping_fee = max(0, (float) ($params['shipping_fee'] ?? 0));
|
||||
$after_sale_policy = sanitize_textarea_field((string) ($params['after_sale_policy'] ?? ''));
|
||||
$support_refund = !isset($params['support_refund']) || !empty($params['support_refund']);
|
||||
$author_id = max(0, (int) ($params['author_id'] ?? 0));
|
||||
|
||||
if ($title === '') {
|
||||
return $this->error('商品名称不能为空');
|
||||
}
|
||||
|
||||
$valid_statuses = ['publish', 'draft', 'pending'];
|
||||
if (!in_array($status, $valid_statuses, true)) {
|
||||
$status = 'draft';
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'aksrt_product',
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
'post_excerpt' => $excerpt,
|
||||
'post_status' => $status,
|
||||
'post_author' => $author_id > 0 ? $author_id : get_current_user_id(),
|
||||
'meta_input' => [
|
||||
'_aksrt_price' => max(0, $price),
|
||||
'_aksrt_stock' => max(0, $stock),
|
||||
'_aksrt_sales' => 0,
|
||||
'_aksrt_score' => 0,
|
||||
'_aksrt_views' => 0,
|
||||
'_aksrt_favorite_count' => 0,
|
||||
'_aksrt_product_gallery' => $gallery,
|
||||
'_aksrt_shipping_mode' => $shipping_mode,
|
||||
'_aksrt_shipping_fee' => $shipping_fee,
|
||||
'_aksrt_after_sale_policy' => $after_sale_policy,
|
||||
'_aksrt_support_refund' => $support_refund ? '1' : '0',
|
||||
'_aksrt_product_video' => is_array($params['product_video'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['product_video']))) : [],
|
||||
'_aksrt_product_params' => sanitize_textarea_field((string) ($params['product_params'] ?? '')),
|
||||
'_aksrt_pay_modo' => sanitize_key((string) ($params['pay_modo'] ?? '0')),
|
||||
'_aksrt_start_price' => max(0, (float) ($params['start_price'] ?? 0)),
|
||||
'_aksrt_product_rebate' => sanitize_text_field((string) ($params['product_rebate'] ?? '')),
|
||||
'_aksrt_limit_buy' => max(-1, min(9999, (int) ($params['limit_buy'] ?? -1))),
|
||||
'_aksrt_limit_buy_note' => sanitize_text_field((string) ($params['limit_buy_note'] ?? '')),
|
||||
'_aksrt_stock_type' => sanitize_key((string) ($params['stock_type'] ?? 'all')),
|
||||
'_aksrt_user_required' => sanitize_textarea_field((string) ($params['user_required'] ?? '')),
|
||||
'_aksrt_shipping_type' => sanitize_key((string) ($params['shipping_type'] ?? $shipping_mode)),
|
||||
'_aksrt_shipping_delivery_desc' => sanitize_text_field((string) ($params['shipping_delivery_desc'] ?? '')),
|
||||
'_aksrt_shipping_fee_type' => sanitize_key((string) ($params['shipping_fee_type'] ?? 'free')),
|
||||
'_aksrt_shipping_fee_free_amount'=> max(0, (float) ($params['shipping_fee_free_amount'] ?? 0)),
|
||||
'_aksrt_after_sale_matrix' => sanitize_textarea_field((string) ($params['after_sale_matrix'] ?? '')),
|
||||
'_aksrt_service' => sanitize_textarea_field((string) ($params['service'] ?? '')),
|
||||
'_aksrt_single_tab' => sanitize_textarea_field((string) ($params['single_tab'] ?? '')),
|
||||
'_aksrt_content_layout' => sanitize_key((string) ($params['content_layout'] ?? '')),
|
||||
'_aksrt_content_show_bg' => sanitize_text_field((string) ($params['content_show_bg'] ?? '')),
|
||||
'_aksrt_delivery_note' => sanitize_text_field((string) ($params['delivery_note'] ?? '')),
|
||||
'_aksrt_marketing_headline' => sanitize_text_field((string) ($params['marketing_headline'] ?? '')),
|
||||
'_aksrt_shipping_tip' => sanitize_text_field((string) ($params['shipping_tip'] ?? '')),
|
||||
'_aksrt_service_tip' => sanitize_text_field((string) ($params['service_tip'] ?? '')),
|
||||
],
|
||||
], true);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return $this->error($post_id->get_error_message());
|
||||
}
|
||||
|
||||
if (!empty($params['categories']) && is_array($params['categories'])) {
|
||||
wp_set_object_terms($post_id, $params['categories'], 'aksrt_product_cat');
|
||||
}
|
||||
if (!empty($params['tags']) && is_array($params['tags'])) {
|
||||
wp_set_object_terms($post_id, $params['tags'], 'aksrt_product_tag');
|
||||
}
|
||||
if ($image !== '') {
|
||||
update_post_meta($post_id, '_aksrt_product_image', $image);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('商品已创建', 'aksrt'),
|
||||
'data' => ['id' => $post_id],
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /shop/products/{id} — 更新商品(含上下架)
|
||||
*/
|
||||
public function update_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$id = (int) $request->get_param('id');
|
||||
$post = get_post($id);
|
||||
|
||||
if (!$post || $post->post_type !== 'aksrt_product') {
|
||||
return $this->error('商品不存在', 404);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$update = ['ID' => $id];
|
||||
|
||||
if (isset($params['title'])) {
|
||||
$update['post_title'] = sanitize_text_field($params['title']);
|
||||
}
|
||||
if (isset($params['content'])) {
|
||||
$update['post_content'] = wp_kses_post($params['content']);
|
||||
}
|
||||
if (isset($params['excerpt'])) {
|
||||
$update['post_excerpt'] = sanitize_textarea_field($params['excerpt']);
|
||||
}
|
||||
if (isset($params['status'])) {
|
||||
$new_status = sanitize_key($params['status']);
|
||||
$valid_statuses = ['publish', 'draft', 'pending', 'trash'];
|
||||
if (in_array($new_status, $valid_statuses, true)) {
|
||||
$update['post_status'] = $new_status;
|
||||
}
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if (isset($params['price'])) {
|
||||
$meta['_aksrt_price'] = max(0, (float) $params['price']);
|
||||
}
|
||||
if (isset($params['stock'])) {
|
||||
$meta['_aksrt_stock'] = max(0, (int) $params['stock']);
|
||||
}
|
||||
if (isset($params['gallery']) && is_array($params['gallery'])) {
|
||||
$meta['_aksrt_product_gallery'] = array_values(array_filter(array_map('esc_url_raw', $params['gallery'])));
|
||||
}
|
||||
if (isset($params['shipping_mode'])) {
|
||||
$meta['_aksrt_shipping_mode'] = sanitize_key((string) $params['shipping_mode']);
|
||||
}
|
||||
if (isset($params['shipping_fee'])) {
|
||||
$meta['_aksrt_shipping_fee'] = max(0, (float) $params['shipping_fee']);
|
||||
}
|
||||
if (isset($params['after_sale_policy'])) {
|
||||
$meta['_aksrt_after_sale_policy'] = sanitize_textarea_field((string) $params['after_sale_policy']);
|
||||
}
|
||||
if (isset($params['support_refund'])) {
|
||||
$meta['_aksrt_support_refund'] = !empty($params['support_refund']) ? '1' : '0';
|
||||
}
|
||||
if (isset($params['product_video']) && is_array($params['product_video'])) {
|
||||
$meta['_aksrt_product_video'] = array_values(array_filter(array_map('esc_url_raw', $params['product_video'])));
|
||||
}
|
||||
if (isset($params['product_params'])) {
|
||||
$meta['_aksrt_product_params'] = sanitize_textarea_field((string) $params['product_params']);
|
||||
}
|
||||
if (isset($params['pay_modo'])) {
|
||||
$meta['_aksrt_pay_modo'] = sanitize_key((string) $params['pay_modo']);
|
||||
}
|
||||
if (isset($params['start_price'])) {
|
||||
$meta['_aksrt_start_price'] = max(0, (float) $params['start_price']);
|
||||
}
|
||||
if (isset($params['product_rebate'])) {
|
||||
$meta['_aksrt_product_rebate'] = sanitize_text_field((string) $params['product_rebate']);
|
||||
}
|
||||
if (isset($params['limit_buy'])) {
|
||||
$meta['_aksrt_limit_buy'] = max(-1, min(9999, (int) $params['limit_buy']));
|
||||
}
|
||||
if (isset($params['limit_buy_note'])) {
|
||||
$meta['_aksrt_limit_buy_note'] = sanitize_text_field((string) $params['limit_buy_note']);
|
||||
}
|
||||
if (isset($params['stock_type'])) {
|
||||
$meta['_aksrt_stock_type'] = sanitize_key((string) $params['stock_type']);
|
||||
}
|
||||
if (isset($params['stock_opts']) && is_array($params['stock_opts'])) {
|
||||
$meta['_aksrt_stock_opts'] = wp_json_encode($params['stock_opts']);
|
||||
}
|
||||
if (isset($params['user_required'])) {
|
||||
$meta['_aksrt_user_required'] = sanitize_textarea_field((string) $params['user_required']);
|
||||
}
|
||||
if (isset($params['shipping_type'])) {
|
||||
$meta['_aksrt_shipping_type'] = sanitize_key((string) $params['shipping_type']);
|
||||
}
|
||||
if (isset($params['shipping_delivery_desc'])) {
|
||||
$meta['_aksrt_shipping_delivery_desc'] = sanitize_text_field((string) $params['shipping_delivery_desc']);
|
||||
}
|
||||
if (isset($params['auto_delivery']) && is_array($params['auto_delivery'])) {
|
||||
$meta['_aksrt_auto_delivery'] = wp_json_encode($params['auto_delivery']);
|
||||
}
|
||||
if (isset($params['shipping_fee_type'])) {
|
||||
$meta['_aksrt_shipping_fee_type'] = sanitize_key((string) $params['shipping_fee_type']);
|
||||
}
|
||||
if (isset($params['shipping_fee_free_amount'])) {
|
||||
$meta['_aksrt_shipping_fee_free_amount'] = max(0, (float) $params['shipping_fee_free_amount']);
|
||||
}
|
||||
if (isset($params['after_sale_matrix'])) {
|
||||
$meta['_aksrt_after_sale_matrix'] = sanitize_textarea_field((string) $params['after_sale_matrix']);
|
||||
}
|
||||
if (isset($params['service'])) {
|
||||
$meta['_aksrt_service'] = sanitize_textarea_field((string) $params['service']);
|
||||
}
|
||||
if (isset($params['single_tab'])) {
|
||||
$meta['_aksrt_single_tab'] = sanitize_textarea_field((string) $params['single_tab']);
|
||||
}
|
||||
if (isset($params['content_layout'])) {
|
||||
$meta['_aksrt_content_layout'] = sanitize_key((string) $params['content_layout']);
|
||||
}
|
||||
if (isset($params['content_show_bg'])) {
|
||||
$meta['_aksrt_content_show_bg'] = sanitize_text_field((string) $params['content_show_bg']);
|
||||
}
|
||||
if (isset($params['delivery_note'])) {
|
||||
$meta['_aksrt_delivery_note'] = sanitize_text_field((string) $params['delivery_note']);
|
||||
}
|
||||
if (isset($params['marketing_headline'])) {
|
||||
$meta['_aksrt_marketing_headline'] = sanitize_text_field((string) $params['marketing_headline']);
|
||||
}
|
||||
if (isset($params['shipping_tip'])) {
|
||||
$meta['_aksrt_shipping_tip'] = sanitize_text_field((string) $params['shipping_tip']);
|
||||
}
|
||||
if (isset($params['service_tip'])) {
|
||||
$meta['_aksrt_service_tip'] = sanitize_text_field((string) $params['service_tip']);
|
||||
}
|
||||
if (!empty($meta)) {
|
||||
$update['meta_input'] = $meta;
|
||||
}
|
||||
|
||||
$result = wp_update_post($update, true);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
}
|
||||
|
||||
if (isset($params['categories']) && is_array($params['categories'])) {
|
||||
wp_set_object_terms($id, $params['categories'], 'aksrt_product_cat');
|
||||
}
|
||||
if (isset($params['tags']) && is_array($params['tags'])) {
|
||||
wp_set_object_terms($id, $params['tags'], 'aksrt_product_tag');
|
||||
}
|
||||
if (isset($params['image'])) {
|
||||
update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image']));
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('商品已更新', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin shop variant and export operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Shop_Controller_Variant_Export_Operations {
|
||||
|
||||
public function get_product_variants(WP_REST_Request $request): WP_REST_Response {
|
||||
$product_id = (int) $request->get_param('id');
|
||||
$product = get_post($product_id);
|
||||
if (!$product || $product->post_type !== 'aksrt_product') {
|
||||
return $this->error('商品不存在', 404);
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->success(['data' => ['variants' => []]]);
|
||||
}
|
||||
|
||||
$variants = AKSRT_Shop::get_instance()->get_variants($product_id);
|
||||
return $this->success(['data' => ['variants' => $variants]]);
|
||||
}
|
||||
|
||||
public function save_product_variants(WP_REST_Request $request): WP_REST_Response {
|
||||
$product_id = (int) $request->get_param('id');
|
||||
$product = get_post($product_id);
|
||||
if (!$product || $product->post_type !== 'aksrt_product') {
|
||||
return $this->error('商品不存在', 404);
|
||||
}
|
||||
|
||||
$params = $request->get_json_params() ?? [];
|
||||
$variants = $params['variants'] ?? [];
|
||||
if (!is_array($variants)) {
|
||||
return $this->error('变体数据格式错误');
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城系统未启用', 501);
|
||||
}
|
||||
|
||||
$saved = AKSRT_Shop::get_instance()->save_variants($product_id, $variants);
|
||||
return $this->success(['message' => '变体已保存', 'data' => ['variants' => $saved]]);
|
||||
}
|
||||
|
||||
public function delete_variant(WP_REST_Request $request): WP_REST_Response {
|
||||
$variant_id = (int) $request->get_param('id');
|
||||
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城系统未启用', 501);
|
||||
}
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->delete_variant($variant_id);
|
||||
if (!$result) {
|
||||
return $this->error('删除失败', 500);
|
||||
}
|
||||
|
||||
return $this->success(['message' => '变体已删除']);
|
||||
}
|
||||
|
||||
public function export_orders(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$args = [
|
||||
'status' => sanitize_key((string) $request->get_param('status')),
|
||||
'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
|
||||
'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
|
||||
'limit' => max(1, min(5000, (int) ($request->get_param('limit') ?: 1000))),
|
||||
];
|
||||
|
||||
$result = AKSRT_Shop::get_instance()->export_orders_csv($args);
|
||||
|
||||
return new WP_REST_Response($result['csv'], 200, [
|
||||
'Content-Type' => 'text/csv; charset=utf-8',
|
||||
'Content-Disposition' => 'attachment; filename="orders-' . date('Ymd-His') . '.csv"',
|
||||
'X-Order-Count' => $result['count'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function confirm_receipt(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$result = AKSRT_Shop::get_instance()->confirm_receipt(get_current_user_id(), $order_number);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? '确认收货失败');
|
||||
}
|
||||
|
||||
return $this->success(['data' => $result['data']]);
|
||||
}
|
||||
|
||||
public function get_after_sale_reasons(WP_REST_Request $request): WP_REST_Response {
|
||||
if (class_exists('AKSRT_After_Sale')) {
|
||||
return $this->success(['data' => AKSRT_After_Sale::get_reason_options()]);
|
||||
}
|
||||
return $this->success(['data' => ['refund' => [], 'return' => []]]);
|
||||
}
|
||||
|
||||
public function get_frontend_after_sale_reasons(WP_REST_Request $request): WP_REST_Response {
|
||||
if (class_exists('AKSRT_After_Sale')) {
|
||||
return $this->success(['data' => AKSRT_After_Sale::get_reason_options()]);
|
||||
}
|
||||
return $this->success(['data' => ['refund' => [], 'return' => []]]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Shop Controller
|
||||
*
|
||||
* Owns REST routes for the "商城&商品" domain.
|
||||
* Provides product CRUD, order management, and shop overview.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Shop_Controller extends AKSRT_Admin_Controller_Base {
|
||||
use AKSRT_Admin_Shop_Controller_Product_Admin_Columns;
|
||||
use AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations;
|
||||
use AKSRT_Admin_Shop_Controller_Product_Operations;
|
||||
use AKSRT_Admin_Shop_Controller_Order_Operations;
|
||||
use AKSRT_Admin_Shop_Controller_Fulfillment_Operations;
|
||||
use AKSRT_Admin_Shop_Controller_Coupon_Gateway_Operations;
|
||||
use AKSRT_Admin_Shop_Controller_Variant_Export_Operations;
|
||||
|
||||
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(AKSRT_Admin $admin): void {
|
||||
$this->admin = $admin;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/checkout/preview', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'preview_checkout'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_frontend_order'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_orders'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)/pay', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'pay_frontend_order'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)/cancel', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'cancel_frontend_order'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_order'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)/after-sale', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_frontend_after_sale'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// ── Address book CRUD (frontend) ──
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_addresses'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_frontend_address'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_frontend_address'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_frontend_address'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)/default', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'set_default_frontend_address'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_frontend_product'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_frontend_product'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// Cart endpoints (§6.4)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/add', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'handle_add_to_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/reduce', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'handle_reduce_from_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'handle_remove_from_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove-multi', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'handle_remove_multi_from_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/empty', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'handle_empty_cart'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/count', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_cart_count'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
// Shop overview / dashboard
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/overview', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_overview'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Product list
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_products'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Product create
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_product'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Product update
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_product'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Product single read
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_product'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Order list
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/orders', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_orders'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Order single read
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/orders/(?P<order_number>[A-Za-z0-9]+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_order'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Order status update — canonical field is order_number
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/orders/update-status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_order_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Gateway status
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/gateways', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_gateways'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Coupon list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_coupons'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Coupon create (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_coupon'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Coupon disable (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/coupons/(?P<id>\d+)/disable', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'disable_coupon'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── After Sale ──
|
||||
|
||||
// After-sale list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_after_sale_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// After-sale create (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_after_sale'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// After-sale status update (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)/update-status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_after_sale_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_after_sale_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Shipping ──
|
||||
|
||||
// Shipping list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_shipping_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Shipping create (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_shipping'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Shipping status update (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)/update-status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_shipping_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_shipping_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/shipping/batch-update-status', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'batch_update_shipping_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
|
||||
// ── R68: Order export + confirm receipt + after-sale reasons ──
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/orders/export', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'export_orders'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── R69: Frontend after-sale reasons (§6.8) ──
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/reasons', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_after_sale_reasons'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)/confirm-receipt', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'confirm_receipt'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_after_sale_reasons'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// ── Product Variants / SKU ──
|
||||
|
||||
// List variants for a product
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_product_variants'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Save variants for a product (replace all)
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'save_product_variants'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Delete a variant
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/variants/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'delete_variant'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_logged_in_permission(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — Tools & Diagnostics Controller
|
||||
*
|
||||
* Handles REST routes for the "备份&导入" and "扩展&增强" domains:
|
||||
* Health checks, email rollback, REST block.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Tools_Controller extends AKSRT_Admin_Controller_Base {
|
||||
|
||||
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)]);
|
||||
|
||||
case 'rebuild_index':
|
||||
// Rebuild search index / recount stats
|
||||
if (class_exists('AKSRT_Forum') && method_exists(AKSRT_Forum::get_instance(), 'recount_stats')) {
|
||||
AKSRT_Forum::get_instance()->recount_stats();
|
||||
}
|
||||
return $this->success(['message' => __('索引重建完成', 'aksrt')]);
|
||||
|
||||
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_Update_Checker')) {
|
||||
$stored = AKSRT_Update_Checker::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_Update_Checker')) {
|
||||
return $this->error(__('更新检查器不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
AKSRT_Update_Checker::get_instance()->do_check();
|
||||
$stored = AKSRT_Update_Checker::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') && AKSRT_Settings::registration_requires_email_verification();
|
||||
$pending_users = 0;
|
||||
|
||||
if (class_exists('AKSRT_Auth_Email_Verification')) {
|
||||
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_Email_Rollback')) {
|
||||
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_Email_Rollback::list_recent_changes($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_Email_Rollback')) {
|
||||
return $this->error(__('邮箱回滚功能不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('id');
|
||||
$result = AKSRT_Email_Rollback::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,287 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin auth and phone operations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Auth_Phone_Operations {
|
||||
|
||||
/**
|
||||
* 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_Private_Message')) {
|
||||
$unread = AKSRT_Private_Message::get_instance()->get_unread_count($user->ID);
|
||||
}
|
||||
|
||||
$credits = 0;
|
||||
if (class_exists('AKSRT_Credits')) {
|
||||
$credits = AKSRT_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'] ?? ''));
|
||||
|
||||
if (!class_exists('AKSRT_SMS')) {
|
||||
return $this->error('短信服务不可用');
|
||||
}
|
||||
|
||||
$result = AKSRT_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
|
||||
/**
|
||||
* AKSRT admin follow operations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Follow_Operations {
|
||||
|
||||
/**
|
||||
* POST /follow/toggle — 切换关注状态
|
||||
*/
|
||||
public function follow_toggle(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_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_Follow::get_instance()->toggle($current_user_id, $target_user_id);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'is_following' => $is_following,
|
||||
'follower_count' => AKSRT_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_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_Follow::get_instance()->is_following($current_user_id, $target_user_id);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'is_following' => $is_following,
|
||||
'follower_count' => AKSRT_Follow::get_instance()->get_follower_count($target_user_id),
|
||||
'following_count' => AKSRT_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_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_Follow::get_instance()->get_follower_count($user_id),
|
||||
'following' => AKSRT_Follow::get_instance()->get_following_count($user_id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin invite operations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Invite_Operations {
|
||||
|
||||
/**
|
||||
* 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_DB_Migrator') ? AKSRT_DB_Migrator::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_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_DB_Migrator') ? AKSRT_DB_Migrator::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,471 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin report operations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Report_Operations {
|
||||
|
||||
public function get_report_list(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Report_Center')) {
|
||||
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_DB_Migrator::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_Report_Center::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_Report_Center')) {
|
||||
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_Report_Center')) {
|
||||
return $this->success(['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0]);
|
||||
}
|
||||
return $this->success(AKSRT_Report_Center::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_DB_Migrator::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_DB_Migrator::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_Report_Center')) {
|
||||
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_Report_Center::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
|
||||
/**
|
||||
* AKSRT admin user restriction operations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Restriction_Operations {
|
||||
|
||||
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,379 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT admin verify and messaging integrations support.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_User_Controller_Verify_Integrations {
|
||||
|
||||
public function get_verify_pending(): WP_REST_Response {
|
||||
$response = $this->get_verify_requests(new WP_REST_Request('GET', '/'));
|
||||
$data = $response->get_data();
|
||||
$items = isset($data['data']['items']) && is_array($data['data']['items']) ? $data['data']['items'] : [];
|
||||
$pending = array_values(array_filter($items, static function ($item) {
|
||||
return ($item['status'] ?? '') === 'pending';
|
||||
}));
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'requests' => array_slice($pending, 0, 50),
|
||||
'total' => count($pending),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_verify_requests(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'));
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$sql = "
|
||||
SELECT u.ID, u.display_name, u.user_email, u.user_registered,
|
||||
status.meta_value AS verify_status,
|
||||
reason.meta_value AS verify_reason,
|
||||
reviewed_at.meta_value AS reviewed_at,
|
||||
reviewer.meta_value AS reviewer_id
|
||||
FROM {$wpdb->users} u
|
||||
INNER JOIN {$wpdb->usermeta} status
|
||||
ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'
|
||||
LEFT JOIN {$wpdb->usermeta} reason
|
||||
ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
|
||||
LEFT JOIN {$wpdb->usermeta} reviewed_at
|
||||
ON reviewed_at.user_id = u.ID AND reviewed_at.meta_key = 'aksrt_user_verify_reviewed_at'
|
||||
LEFT JOIN {$wpdb->usermeta} reviewer
|
||||
ON reviewer.user_id = u.ID AND reviewer.meta_key = 'aksrt_user_verify_reviewer'
|
||||
ORDER BY u.user_registered DESC
|
||||
LIMIT 500
|
||||
";
|
||||
|
||||
$rows = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
$counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'total' => 0];
|
||||
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_verify_request_item($row);
|
||||
$item_status = $item['status'];
|
||||
if (isset($counts[$item_status])) {
|
||||
$counts[$item_status]++;
|
||||
}
|
||||
$counts['total']++;
|
||||
|
||||
if ($status !== '' && $item_status !== $status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($date_from !== '' && strcmp((string) $item['requested_at'], $date_from . ' 00:00:00') < 0) {
|
||||
continue;
|
||||
}
|
||||
if ($date_to !== '' && strcmp((string) $item['requested_at'], $date_to . ' 23:59:59') > 0) {
|
||||
continue;
|
||||
}
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', [
|
||||
(string) ($item['display_name'] ?? ''),
|
||||
(string) ($item['email'] ?? ''),
|
||||
(string) ($item['reason'] ?? ''),
|
||||
]), $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values(array_slice($items, $offset, $per_page)),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'counts' => $counts,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
|
||||
['label' => __('已通过', 'aksrt'), 'value' => 'verified'],
|
||||
['label' => __('已驳回', 'aksrt'), 'value' => 'rejected'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_verify_request_detail(WP_REST_Request $request): WP_REST_Response {
|
||||
$user_id = (int) $request->get_param('id');
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('认证申请不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return $this->error(__('用户不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$detail = $this->build_verify_request_item([
|
||||
'ID' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'user_email' => $user->user_email,
|
||||
'user_registered' => $user->user_registered,
|
||||
'verify_status' => get_user_meta($user_id, 'aksrt_user_verify_status', true),
|
||||
'verify_reason' => get_user_meta($user_id, 'aksrt_user_verify_reason', true),
|
||||
'reviewed_at' => get_user_meta($user_id, 'aksrt_user_verify_reviewed_at', true),
|
||||
'reviewer_id' => get_user_meta($user_id, 'aksrt_user_verify_reviewer', true),
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'request' => $detail,
|
||||
'history' => $this->get_verify_history_for_user($user_id),
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('通过认证', 'aksrt'), 'value' => 'verified'],
|
||||
['label' => __('驳回申请', 'aksrt'), 'value' => 'rejected'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /verify/review — approve or reject a verification request.
|
||||
*/
|
||||
public function review_verify_request(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$user_id = (int) ($params['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->perform_verify_review($user_id, $params);
|
||||
}
|
||||
|
||||
public function review_verify_request_by_id(WP_REST_Request $request): WP_REST_Response {
|
||||
$user_id = (int) $request->get_param('id');
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->perform_verify_review($user_id, $this->get_json_params($request));
|
||||
}
|
||||
|
||||
private function perform_verify_review(int $user_id, array $payload): WP_REST_Response {
|
||||
$status = sanitize_key((string) ($payload['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($payload['note'] ?? ($payload['reason'] ?? '')));
|
||||
|
||||
if (!in_array($status, ['verified', 'rejected'], true)) {
|
||||
return $this->error(__('审核状态无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return $this->error(__('用户不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
update_user_meta($user_id, 'aksrt_user_verify_status', $status);
|
||||
update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
|
||||
update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
|
||||
|
||||
if ($note !== '') {
|
||||
update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
|
||||
} elseif ($status === 'verified') {
|
||||
delete_user_meta($user_id, 'aksrt_user_verify_reason');
|
||||
}
|
||||
|
||||
$log = get_option('aksrt_verify_review_log', []);
|
||||
if (!is_array($log)) {
|
||||
$log = [];
|
||||
}
|
||||
$log[] = [
|
||||
'user_id' => $user_id,
|
||||
'reviewer' => get_current_user_id(),
|
||||
'status' => $status,
|
||||
'reason' => $note,
|
||||
'time' => current_time('mysql'),
|
||||
];
|
||||
if (count($log) > 500) {
|
||||
$log = array_slice($log, -500);
|
||||
}
|
||||
update_option('aksrt_verify_review_log', $log, false);
|
||||
|
||||
return $this->success([
|
||||
'message' => $status === 'verified'
|
||||
? __('已通过认证', 'aksrt')
|
||||
: __('已驳回申请', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_verify_request_item(array $row): array {
|
||||
$user_id = (int) ($row['ID'] ?? 0);
|
||||
$reviewer_id = (int) ($row['reviewer_id'] ?? 0);
|
||||
$reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
|
||||
$status = sanitize_key((string) ($row['verify_status'] ?? 'pending'));
|
||||
$email_verified = (bool) get_user_meta($user_id, 'aksrt_email_verified', true);
|
||||
|
||||
return [
|
||||
'user_id' => $user_id,
|
||||
'display_name' => (string) ($row['display_name'] ?? ''),
|
||||
'email' => (string) ($row['user_email'] ?? ''),
|
||||
'status' => $status,
|
||||
'reason' => (string) ($row['verify_reason'] ?? ''),
|
||||
'requested_at' => (string) ($row['user_registered'] ?? ''),
|
||||
'reviewed_at' => (string) ($row['reviewed_at'] ?? ''),
|
||||
'email_verified' => $email_verified,
|
||||
'reviewer' => $this->build_admin_user_summary($reviewer_id, $reviewer),
|
||||
'history_count' => count($this->get_verify_history_for_user($user_id)),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_verify_history_for_user(int $user_id): array {
|
||||
$log = get_option('aksrt_verify_review_log', []);
|
||||
if (!is_array($log)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$history = [];
|
||||
foreach ($log as $entry) {
|
||||
if ((int) ($entry['user_id'] ?? 0) !== $user_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reviewer_id = (int) ($entry['reviewer'] ?? 0);
|
||||
$history[] = [
|
||||
'status' => sanitize_key((string) ($entry['status'] ?? '')),
|
||||
'reason' => (string) ($entry['reason'] ?? ''),
|
||||
'time' => (string) ($entry['time'] ?? ''),
|
||||
'reviewer' => $reviewer_id,
|
||||
'reviewer_name' => $reviewer_id > 0 && ($user = get_userdata($reviewer_id)) ? $user->display_name : '',
|
||||
];
|
||||
}
|
||||
|
||||
usort($history, static function ($left, $right) {
|
||||
return strcmp((string) ($right['time'] ?? ''), (string) ($left['time'] ?? ''));
|
||||
});
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
private function build_admin_user_summary(int $user_id, $user = null): ?array {
|
||||
if ($user_id <= 0 && !$user instanceof WP_User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$user instanceof WP_User) {
|
||||
$user = get_userdata($user_id);
|
||||
}
|
||||
if (!$user instanceof WP_User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => (string) $user->display_name,
|
||||
'email' => (string) $user->user_email,
|
||||
'avatar_url' => get_avatar_url((int) $user->ID, ['size' => 64]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sms/status — SMS provider connection status.
|
||||
*/
|
||||
public function get_sms_status(): WP_REST_Response {
|
||||
if (class_exists('AKSRT_SMS')) {
|
||||
return $this->success(['data' => AKSRT_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')) {
|
||||
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::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_WeChat_Notify')) {
|
||||
return $this->success(['data' => AKSRT_WeChat_Notify::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::get_wechat_settings();
|
||||
if ($settings['app_secret'] === '***') {
|
||||
$settings['app_secret'] = $existing['app_secret'] ?? '';
|
||||
}
|
||||
|
||||
$merged = array_merge($existing, $settings);
|
||||
AKSRT_Settings::update_wechat_settings($merged);
|
||||
delete_transient('aksrt_wechat_token');
|
||||
|
||||
return $this->success(['message' => __('微信设置已保存', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Admin — User & Interaction Controller
|
||||
*
|
||||
* Handles REST routes for the "用户&互动" domain:
|
||||
* Auth, OAuth, OAuth proxy, notifications, credits, user background.
|
||||
*
|
||||
* Captcha, moderation, and user-permissions routes have been moved to
|
||||
* AKSRT_Admin_Permissions_Controller for proper domain separation.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_User_Controller extends AKSRT_Admin_Controller_Base {
|
||||
use AKSRT_Admin_User_Controller_Report_Operations;
|
||||
use AKSRT_Admin_User_Controller_Invite_Operations;
|
||||
use AKSRT_Admin_User_Controller_Verify_Integrations;
|
||||
use AKSRT_Admin_User_Controller_Auth_Phone_Operations;
|
||||
use AKSRT_Admin_User_Controller_Restriction_Operations;
|
||||
use AKSRT_Admin_User_Controller_Follow_Operations;
|
||||
|
||||
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 background upload (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(); },
|
||||
]);
|
||||
|
||||
// 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(); },
|
||||
]);
|
||||
|
||||
// Verify: list pending verification requests
|
||||
register_rest_route(self::API_NAMESPACE, '/verify/pending', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_verify_pending'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/verify/requests', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_verify_requests'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_verify_request_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Verify: approve or reject a verification request
|
||||
register_rest_route(self::API_NAMESPACE, '/verify/review', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'review_verify_request'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P<id>\d+)/review', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'review_verify_request_by_id'],
|
||||
'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_Credits_Runtime::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 = AKSRT_Admin_Schema_Provider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'auth');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = AKSRT_Settings::get_all();
|
||||
$invite = class_exists('AKSRT_Settings_Credits_Runtime')
|
||||
? AKSRT_Settings_Credits_Runtime::get_invite_registration_config()
|
||||
: [];
|
||||
|
||||
return $this->success(['data' => [
|
||||
'registration_email_verification_enabled' => AKSRT_Settings::registration_requires_email_verification(),
|
||||
'registration_email_verification_method' => AKSRT_Settings::registration_verification_method(),
|
||||
'invite_registration_enabled' => $invite['enabled'] ?? true,
|
||||
'invite_code_length' => class_exists('AKSRT_Settings') ? AKSRT_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::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::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('注册登录配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_handle_notification_settings(WP_REST_Request $request): WP_REST_Response {
|
||||
$provider = AKSRT_Admin_Schema_Provider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'notifications');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = AKSRT_Settings::get_all();
|
||||
return $this->success(['data' => array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = AKSRT_Settings::get_all();
|
||||
$settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
|
||||
update_option('aksrt_settings', $settings);
|
||||
AKSRT_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_Message')) {
|
||||
$args = ['limit' => $per_page, 'offset' => ($page - 1) * $per_page];
|
||||
if ($type !== '') {
|
||||
$args['type'] = $type;
|
||||
}
|
||||
$items = AKSRT_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_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_Message')) {
|
||||
return $this->success(['message' => __('消息系统未启用', 'aksrt')]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$msg = AKSRT_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 {
|
||||
return $this->success(['message' => __('当前版本暂未提供后台删除单条消息能力', 'aksrt')]);
|
||||
}
|
||||
|
||||
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 = AKSRT_Admin_Schema_Provider::get_instance();
|
||||
$allowed = $provider->get_allowed_fields('users', 'oauth');
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = AKSRT_Settings::get_all();
|
||||
return $this->success(['data' => array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$settings = AKSRT_Settings::get_all();
|
||||
$settings = array_merge($settings, $this->sanitize_fields($params, $allowed));
|
||||
update_option('aksrt_settings', $settings);
|
||||
AKSRT_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::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::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::flush_settings_cache();
|
||||
|
||||
return $this->success(['message' => __('OAuth 代理配置已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_upload_user_background(WP_REST_Request $request): WP_REST_Response {
|
||||
return $this->error(__('当前版本暂未提供后台 REST 背景图上传,请使用标准媒体上传流程。', 'aksrt'), 501);
|
||||
}
|
||||
|
||||
public function rest_upload_comment_image(WP_REST_Request $request): WP_REST_Response {
|
||||
return $this->error(__('当前版本暂未提供后台 REST 评论图片上传,请使用标准媒体上传流程。', 'aksrt'), 501);
|
||||
}
|
||||
|
||||
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') ? AKSRT_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') ? AKSRT_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')) {
|
||||
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::add($user_id, $delta, $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
|
||||
} else {
|
||||
$result = AKSRT_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') ? AKSRT_Credits::get_log($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Forum_Controller_Part_1 {
|
||||
|
||||
public function update_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
|
||||
$result = $forum->update_plate($plate_id, $params);
|
||||
if (!$result) {
|
||||
return $this->error(__('更新版块失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('版块已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function delete_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
|
||||
$move_to = (int) ($params['move_to'] ?? 0);
|
||||
|
||||
$result = $forum->delete_plate($plate_id, $cascade, $move_to);
|
||||
if (!$result) {
|
||||
return $this->error(__('删除版块失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('版块已删除', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function reorder_plates(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$order = $params['order'] ?? [];
|
||||
if (!is_array($order) || empty($order)) {
|
||||
return $this->error(__('排序参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$forum->reorder_plates($order);
|
||||
|
||||
return $this->success(['message' => __('排序已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function toggle_follow_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
$result = $forum->toggle_follow_plate($user_id, $plate_id);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function get_plate_categories(): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => []]);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $forum->get_plate_categories()]);
|
||||
}
|
||||
|
||||
public function get_sorted_plates(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => []]);
|
||||
}
|
||||
|
||||
$orderby = sanitize_key((string) ($request->get_param('orderby') ?: 'menu_order'));
|
||||
$order = sanitize_key((string) ($request->get_param('order') ?: 'ASC'));
|
||||
$limit = max(1, min(100, (int) ($request->get_param('limit') ?: 50)));
|
||||
|
||||
return $this->success(['data' => $forum->get_sorted_plates($orderby, $order, $limit)]);
|
||||
}
|
||||
|
||||
public function get_topics(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
|
||||
}
|
||||
|
||||
$args = [
|
||||
'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') ?: 'all')),
|
||||
'board_id' => (int) ($request->get_param('board_id') ?: 0),
|
||||
'visibility' => sanitize_key((string) ($request->get_param('visibility') ?: '')),
|
||||
'review_status' => sanitize_key((string) ($request->get_param('review_status') ?: '')),
|
||||
'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
|
||||
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?: 'date')),
|
||||
'viewer_user_id' => get_current_user_id(),
|
||||
'include_hidden' => true,
|
||||
];
|
||||
|
||||
$view = $forum->get_topic_list($args);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $view['items'],
|
||||
'total' => $view['total'],
|
||||
'page' => $view['page'],
|
||||
'total_pages' => $view['max_pages'],
|
||||
'options' => [
|
||||
'boards' => $forum->get_boards(),
|
||||
'visibility' => $forum->get_visibility_options(),
|
||||
'review' => $forum->get_review_options(),
|
||||
'status' => [
|
||||
['label' => __('全部', 'aksrt'), 'value' => 'all'],
|
||||
['label' => __('已发布', 'aksrt'), 'value' => 'publish'],
|
||||
['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
|
||||
['label' => __('草稿/拒绝', 'aksrt'), 'value' => 'draft'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_topic_detail(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
if ($topic_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => $forum->get_topic_detail($topic_id, get_current_user_id()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function moderate_topic(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
||||
|
||||
if ($topic_id <= 0 || $action === '') {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$result = $forum->moderate_topic($topic_id, $action, $params, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message(), $result->get_error_code() === 'forbidden' ? 403 : 400);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? __('治理动作已执行', 'aksrt'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function vote_topic(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$option_ids = $params['option_ids'] ?? [];
|
||||
if (!is_array($option_ids)) {
|
||||
$option_ids = [];
|
||||
}
|
||||
|
||||
$result = $forum->submit_vote($topic_id, get_current_user_id(), $option_ids);
|
||||
if (is_wp_error($result)) {
|
||||
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? __('投票成功', 'aksrt'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy admin moderation endpoint kept for compatibility.
|
||||
*/
|
||||
public function update_topic_legacy(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$topic_id = (int) ($params['topic_id'] ?? 0);
|
||||
if ($topic_id <= 0) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
if (isset($params['sticky'])) {
|
||||
$actions[] = ['action' => 'sticky', 'value' => !empty($params['sticky'])];
|
||||
}
|
||||
if (isset($params['featured'])) {
|
||||
$actions[] = ['action' => 'featured', 'value' => !empty($params['featured'])];
|
||||
}
|
||||
if (isset($params['locked'])) {
|
||||
$actions[] = ['action' => 'locked', 'value' => !empty($params['locked'])];
|
||||
}
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$result = $forum->moderate_topic($topic_id, $action['action'], ['value' => $action['value']], get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('治理状态已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function get_moderators(): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => ['boards' => []]]);
|
||||
}
|
||||
|
||||
$board_id = isset($_GET['board_id']) ? (int) $_GET['board_id'] : 0;
|
||||
$result = [];
|
||||
foreach ($forum->get_boards() as $board) {
|
||||
$current_board_id = (int) ($board['id'] ?? 0);
|
||||
if ($board_id > 0 && $current_board_id !== $board_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'board_id' => $current_board_id,
|
||||
'board_name' => (string) ($board['name'] ?? ''),
|
||||
'topic_count' => (int) ($board['post_count'] ?? 0),
|
||||
'pending_application_count' => (int) ($board['pending_application_count'] ?? 0),
|
||||
'moderators' => $forum->get_board_moderator_profiles_with_roles($current_board_id),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success(['data' => ['boards' => $result]]);
|
||||
}
|
||||
|
||||
public function update_moderators(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$board_id = (int) ($params['board_id'] ?? 0);
|
||||
$user_id = (int) ($params['user_id'] ?? 0);
|
||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
||||
$role = sanitize_key((string) ($params['role'] ?? 'moderator'));
|
||||
|
||||
if ($board_id <= 0 || $user_id <= 0 || !in_array($action, ['add', 'remove'], true)) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
|
||||
if (!in_array($role, $valid_roles, true)) {
|
||||
$role = 'moderator';
|
||||
}
|
||||
|
||||
$board = get_term($board_id, 'aksrt_forum_board');
|
||||
if (!$board || is_wp_error($board)) {
|
||||
return $this->error(__('版块不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return $this->error(__('用户不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
if ($action === 'add') {
|
||||
$forum->add_moderator($user_id, $board_id, $role);
|
||||
} else {
|
||||
$forum->remove_moderator($user_id, $board_id);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $action === 'add' ? __('版主已添加', 'aksrt') : __('版主已移除', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function change_moderator_role(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$board_id = (int) ($params['board_id'] ?? 0);
|
||||
$user_id = (int) ($params['user_id'] ?? 0);
|
||||
$role = sanitize_key((string) ($params['role'] ?? ''));
|
||||
|
||||
if ($board_id <= 0 || $user_id <= 0 || !$role) {
|
||||
return $this->error(__('参数无效', 'aksrt'));
|
||||
}
|
||||
|
||||
$valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
|
||||
if (!in_array($role, $valid_roles, true)) {
|
||||
return $this->error(__('无效的角色', 'aksrt'));
|
||||
}
|
||||
|
||||
$result = $forum->set_moderator_role($user_id, $board_id, $role);
|
||||
if (!$result) {
|
||||
return $this->error(__('设置角色失败,用户可能不是该版块版主', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('角色已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function apply_moderator(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$board_id = (int) ($params['board_id'] ?? 0);
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
|
||||
$result = $forum->apply_moderator(get_current_user_id(), $board_id, $reason);
|
||||
if (is_wp_error($result)) {
|
||||
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? __('申请已提交', 'aksrt'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_moderator_applications(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0]]);
|
||||
}
|
||||
|
||||
$items = $forum->get_moderator_application_list([
|
||||
'status' => sanitize_key((string) ($request->get_param('status') ?: '')),
|
||||
'board_id' => (int) ($request->get_param('board_id') ?: 0),
|
||||
'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => count($items),
|
||||
'options' => [
|
||||
'boards' => $forum->get_boards(),
|
||||
'status' => [
|
||||
['label' => __('全部', 'aksrt'), 'value' => ''],
|
||||
['label' => __('待处理', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_PENDING],
|
||||
['label' => __('已通过', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_APPROVED],
|
||||
['label' => __('已拒绝', 'aksrt'), 'value' => AKSRT_Forum::APPLICATION_REJECTED],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Forum_Controller_Part_2 {
|
||||
|
||||
|
||||
public function review_moderator_application(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$application_id = (string) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$status = sanitize_key((string) ($params['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
|
||||
$result = $forum->review_moderator_application($application_id, $status, $note, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? __('申请状态已更新', 'aksrt'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggle_favorite(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
$result = $forum->toggle_favorite(get_current_user_id(), $topic_id);
|
||||
if (is_wp_error($result)) {
|
||||
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function score_topic(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$amount = (int) ($params['amount'] ?? 0);
|
||||
|
||||
$result = $forum->score_topic(get_current_user_id(), $topic_id, $amount);
|
||||
if (is_wp_error($result)) {
|
||||
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function adopt_answer(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$topic_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$comment_id = (int) ($params['comment_id'] ?? 0);
|
||||
|
||||
$result = $forum->adopt_answer($topic_id, $comment_id, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
$status = $result->get_error_code() === 'forbidden' ? 403 : 400;
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function check_logged_in_permission(): bool {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
private function get_forum_service(): ?AKSRT_Forum {
|
||||
return class_exists('AKSRT_Forum') ? AKSRT_Forum::get_instance() : null;
|
||||
}
|
||||
|
||||
public function frontend_create_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if (!$forum->is_forum_admin($user_id)) {
|
||||
return $this->error(__('仅论坛管理员可创建版块', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$data = [
|
||||
'name' => sanitize_text_field((string) ($params['name'] ?? '')),
|
||||
'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
|
||||
'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
|
||||
'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
|
||||
'parent_id' => (int) ($params['parent_id'] ?? 0),
|
||||
'plate_cat' => (int) ($params['plate_cat'] ?? 0),
|
||||
];
|
||||
|
||||
if ($data['name'] === '') {
|
||||
return $this->error(__('版块名称不能为空', 'aksrt'));
|
||||
}
|
||||
|
||||
$result = $forum->create_plate($data);
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function frontend_update_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
|
||||
$post = get_post($plate_id);
|
||||
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
|
||||
return $this->error(__('版块不存在', 'aksrt'));
|
||||
}
|
||||
|
||||
$is_author = (int) $post->post_author === $user_id;
|
||||
if (!$is_author && !$forum->is_forum_admin($user_id)) {
|
||||
return $this->error(__('无权编辑此版块', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$data = [
|
||||
'name' => sanitize_text_field((string) ($params['name'] ?? '')),
|
||||
'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
|
||||
'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
|
||||
'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
|
||||
];
|
||||
|
||||
$result = $forum->update_plate($plate_id, $data);
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function frontend_delete_plate(WP_REST_Request $request): WP_REST_Response {
|
||||
$forum = $this->get_forum_service();
|
||||
if (!$forum) {
|
||||
return $this->error(__('论坛系统未启用', 'aksrt'));
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if (!$forum->is_forum_admin($user_id)) {
|
||||
return $this->error(__('仅论坛管理员可删除版块', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
$plate_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
|
||||
$move_to = (int) ($params['move_to'] ?? 0);
|
||||
|
||||
$result = $forum->delete_plate($plate_id, $cascade, $move_to);
|
||||
if (!$result) {
|
||||
return $this->error(__('删除版块失败', 'aksrt'));
|
||||
}
|
||||
|
||||
return $this->success(['message' => __('版块已删除', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Global_Controller_Part_1 {
|
||||
|
||||
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')
|
||||
? AKSRT_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')) {
|
||||
AKSRT_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')) {
|
||||
AKSRT_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', 'shop_cart'];
|
||||
$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 = '';
|
||||
}
|
||||
|
||||
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' => esc_url_raw($item['link'] ?? ''),
|
||||
'add_items' => $add_items,
|
||||
'hover' => wp_kses_post($item['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,258 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Global_Controller_Part_2 {
|
||||
|
||||
|
||||
public function rest_handle_homepage_settings(WP_REST_Request $request): WP_REST_Response {
|
||||
$option_key = 'aksrt_settings';
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = AKSRT_Settings::get_homepage_fixed_settings();
|
||||
$data['homepage_modules'] = AKSRT_Settings::get_homepage_modules();
|
||||
return rest_ensure_response([
|
||||
'code' => 'success',
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
if (empty($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
|
||||
$allowed_sorts = AKSRT_Settings::homepage_allowed_sorts();
|
||||
|
||||
$settings = [
|
||||
'homepage_card_style' => in_array($params['homepage_card_style'] ?? '', ['default', 'style2', 'style3'], true)
|
||||
? $params['homepage_card_style']
|
||||
: 'default',
|
||||
'homepage_multi_thumb_enabled' => !isset($params['homepage_multi_thumb_enabled']) || !empty($params['homepage_multi_thumb_enabled']),
|
||||
'homepage_multi_thumb_min_images' => max(2, min(4, (int) ($params['homepage_multi_thumb_min_images'] ?? 3))),
|
||||
'homepage_sidebar_mini_enabled' => !isset($params['homepage_sidebar_mini_enabled']) || !empty($params['homepage_sidebar_mini_enabled']),
|
||||
'homepage_default_sort' => in_array($params['homepage_default_sort'] ?? '', $allowed_sorts, true)
|
||||
? $params['homepage_default_sort']
|
||||
: 'modified',
|
||||
'homepage_default_view' => in_array($params['homepage_default_view'] ?? '', ['list', 'card'], true)
|
||||
? $params['homepage_default_view']
|
||||
: 'list',
|
||||
'homepage_sort_ajax_enabled' => !isset($params['homepage_sort_ajax_enabled']) || !empty($params['homepage_sort_ajax_enabled']),
|
||||
'homepage_sticky_tabs_enabled' => !isset($params['homepage_sticky_tabs_enabled']) || !empty($params['homepage_sticky_tabs_enabled']),
|
||||
'homepage_content_model' => in_array($params['homepage_content_model'] ?? '', ['post', 'mixed'], true)
|
||||
? $params['homepage_content_model']
|
||||
: 'mixed',
|
||||
'home_posts_list_enabled' => !isset($params['home_posts_list_enabled']) || !empty($params['home_posts_list_enabled']),
|
||||
'home_list_title' => sanitize_text_field((string) ($params['home_list_title'] ?? __('Latest Posts', 'aksrt'))),
|
||||
'home_list_sort_enabled' => !isset($params['home_list_sort_enabled']) || !empty($params['home_list_sort_enabled']),
|
||||
'home_list_sort_dropdown' => !empty($params['home_list_sort_dropdown']),
|
||||
'homepage_tab_posts_per_page' => max(1, min(20, (int) ($params['homepage_tab_posts_per_page'] ?? 6))),
|
||||
'homepage_tab_limit' => max(1, min(12, (int) ($params['homepage_tab_limit'] ?? 4))),
|
||||
];
|
||||
|
||||
if (isset($params['home_list_sort_options']) && is_array($params['home_list_sort_options'])) {
|
||||
$seen = [];
|
||||
$options = [];
|
||||
foreach ($params['home_list_sort_options'] as $key) {
|
||||
$key = sanitize_key((string) $key);
|
||||
if (in_array($key, $allowed_sorts, true) && !isset($seen[$key])) {
|
||||
$options[] = $key;
|
||||
$seen[$key] = true;
|
||||
}
|
||||
}
|
||||
$settings['home_list_sort_options'] = !empty($options) ? $options : ['modified', 'date', 'views', 'likes', 'comments'];
|
||||
}
|
||||
|
||||
if (isset($params['home_exclude_post_ids'])) {
|
||||
$settings['home_exclude_post_ids'] = array_values(array_filter(
|
||||
array_map('absint', (array) $params['home_exclude_post_ids']),
|
||||
static fn($value) => $value > 0
|
||||
));
|
||||
}
|
||||
|
||||
if (isset($params['home_exclude_category_ids'])) {
|
||||
$settings['home_exclude_category_ids'] = array_values(array_filter(
|
||||
array_map('absint', (array) $params['home_exclude_category_ids']),
|
||||
static fn($value) => $value > 0
|
||||
));
|
||||
}
|
||||
|
||||
if (isset($params['homepage_tabs']) && is_array($params['homepage_tabs'])) {
|
||||
$tabs = [];
|
||||
foreach ($params['homepage_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, 'category');
|
||||
if (!$term || is_wp_error($term)) {
|
||||
continue;
|
||||
}
|
||||
$orderby = sanitize_key($tab['orderby'] ?? 'date');
|
||||
if (!in_array($orderby, $allowed_sorts, true)) {
|
||||
$orderby = 'date';
|
||||
}
|
||||
$tabs[] = [
|
||||
'term_id' => $term_id,
|
||||
'title' => sanitize_text_field($tab['title'] ?? ''),
|
||||
'orderby' => $orderby,
|
||||
'enabled' => !empty($tab['enabled']),
|
||||
];
|
||||
}
|
||||
$settings['homepage_tabs'] = $tabs;
|
||||
}
|
||||
|
||||
if (isset($params['homepage_modules']) && is_array($params['homepage_modules'])) {
|
||||
$modules = [];
|
||||
$allowed_types = [
|
||||
'hero', 'banner', 'category', 'tabs', 'ranking', 'recent',
|
||||
'tag_cloud', 'friend_links', 'html', 'hotspot',
|
||||
'category_recommend_cards', 'text_title', 'button_group',
|
||||
'spacing', 'faq', 'data_grid', 'highlight_cards',
|
||||
'partner_marquee', 'product_showcase',
|
||||
];
|
||||
foreach ($params['homepage_modules'] as $module) {
|
||||
if (!is_array($module) || !in_array($module['type'] ?? '', $allowed_types, true)) {
|
||||
continue;
|
||||
}
|
||||
$modules[] = [
|
||||
'id' => sanitize_text_field($module['id'] ?? uniqid('mod_')),
|
||||
'type' => sanitize_text_field($module['type']),
|
||||
'config' => is_array($module['config'] ?? null) ? $module['config'] : [],
|
||||
'enabled' => !empty($module['enabled']),
|
||||
'title' => sanitize_text_field($module['title'] ?? ''),
|
||||
];
|
||||
}
|
||||
$settings['homepage_modules'] = $modules;
|
||||
}
|
||||
|
||||
$current = get_option($option_key, []);
|
||||
if (!is_array($current)) {
|
||||
$current = [];
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
$current[$key] = $value;
|
||||
}
|
||||
|
||||
update_option($option_key, $current);
|
||||
AKSRT_Settings::flush_settings_cache();
|
||||
update_option('aksrt_homepage_schema_version', gmdate('Y-m-d'));
|
||||
delete_transient('aksrt_homepage_cached_images');
|
||||
delete_transient('aksrt_homepage_modules_cache');
|
||||
do_action('aksrt_homepage_settings_changed');
|
||||
|
||||
return rest_ensure_response([
|
||||
'code' => 'success',
|
||||
'message' => __('Homepage settings saved.', 'aksrt'),
|
||||
'data' => $settings,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_handle_category_recommend(WP_REST_Request $request): WP_REST_Response {
|
||||
if ($request->get_method() === 'GET') {
|
||||
$settings = AKSRT_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::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::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::flush_category_recommend_cache();
|
||||
|
||||
return rest_ensure_response(['code' => 'success', 'message' => __('分类元数据已保存', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Import_Export_Controller_Part_1 {
|
||||
|
||||
public function rest_migration_status(WP_REST_Request $request): WP_REST_Response {
|
||||
$schema_version = get_option('aksrt_db_schema_version', '0.0.0');
|
||||
$migration = class_exists('AKSRT_Settings_Migration') ? AKSRT_Settings_Migration::get_instance() : null;
|
||||
$migration_log = $migration ? $migration->get_log() : get_option('aksrt_settings_migration_log', []);
|
||||
|
||||
return $this->success([
|
||||
'schema_version' => $schema_version,
|
||||
'migration_version' => $migration ? $migration->get_version() : get_option('aksrt_settings_migration_version', '0.0.0'),
|
||||
'migration_log' => is_array($migration_log) ? $migration_log : [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = AKSRT_Admin_Schema_Provider::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 = AKSRT_Admin_Schema_Provider::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::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;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Admin_Import_Export_Controller_Part_2 {
|
||||
|
||||
|
||||
private function get_backup_schedule_settings(): array {
|
||||
$provider = AKSRT_Admin_Schema_Provider::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 = AKSRT_Admin_Schema_Provider::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::STANDALONE_OPTION_KEYS as $key) {
|
||||
$value = get_option($key);
|
||||
if ($value !== false) {
|
||||
$payload['standalone'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
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::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,43 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions_Audit section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions_Audit {
|
||||
|
||||
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' => 'audit_bbs_term', '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
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions_Docs_Mode section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions_Docs_Mode {
|
||||
|
||||
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', 'options' => [
|
||||
['label' => '文档导航页', 'value' => 'documentnav'],
|
||||
['label' => '专题聚合页', 'value' => 'special'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'navigation', 'title' => '导航设置', 'fields' => [
|
||||
['key' => 'docs_mode_toc_enabled', 'label' => '显示目录树', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'docs_mode_chapter_nav_enabled', 'label' => '显示章节导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'docs_mode_default_order', 'label' => '默认排序', 'type' => 'radio', 'default' => 'menu_order', '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],
|
||||
['key' => 'docs_mode_chapter_title', 'label' => '章节标题', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'filter', 'title' => '筛选设置', 'fields' => [
|
||||
['key' => 'docs_mode_cats', 'label' => '文档分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
|
||||
['key' => 'docs_mode_exclude', 'label' => '在首页排除此类内容', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'style', 'title' => '样式设置', 'fields' => [
|
||||
['key' => 'docs_mode_style', 'label' => '文档样式', 'type' => 'select', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '简洁', 'value' => 'simple'],
|
||||
['label' => '卡片', 'value' => 'card'],
|
||||
]],
|
||||
['key' => 'docs_mode_icon', 'label' => '显示图标', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'docs_mode_count', 'label' => '显示文章数', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions_IP section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions_IP {
|
||||
|
||||
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' => ''],
|
||||
['key' => 'ip_addr_sdk_qq_secretkey', 'label' => '腾讯 SecretKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'ip_addr_sdk_amap_appkey', 'label' => '高德 AppKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'ip_addr_sdk_amap_secretkey', 'label' => '高德 SecretKey', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'ip_geo_display', 'label' => '展示位置', 'type' => 'checkbox', 'default' => ['comment'], 'sanitize_type' => 'array', 'options' => [
|
||||
['label' => '评论区', 'value' => 'comment'],
|
||||
['label' => '用户主页', 'value' => 'user_home'],
|
||||
['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
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions_Push section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions_Push {
|
||||
|
||||
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,111 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions_Security section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions_Security {
|
||||
|
||||
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' => 'verification', 'title' => '验证设置', 'fields' => [
|
||||
['key' => 'verification_comment_s', 'label' => '评论需要验证', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'verification_links_s', 'label' => '链接需要验证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'verification_newposts_s', 'label' => '新文章需要验证', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'verification_bbspost_s', 'label' => '论坛帖子需要验证', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'verification_signin_exclude', 'label' => '登录人机验证排除账号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_verification_type', 'label' => '验证类型', 'type' => 'radio', 'default' => 'slider', 'options' => [
|
||||
['label' => '图形验证码', 'value' => 'image'],
|
||||
['label' => '滑块验证', 'value' => 'slider'],
|
||||
['label' => '腾讯验证码', 'value' => 'tcaptcha'],
|
||||
['label' => '极验验证码', 'value' => 'geetest'],
|
||||
['label' => '关闭验证', 'value' => 'null'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'captcha', 'title' => '验证码设置', 'fields' => [
|
||||
['key' => 'geetest_enabled', 'label' => '极验验证码', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'geetest_id', 'label' => '极验 ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'geetest_key', 'label' => '极验 Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'tcaptcha_enabled', 'label' => '腾讯云验证码', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'tcaptcha_appid', 'label' => '腾讯云 AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'tcaptcha_secret_id', 'label' => '腾讯云 SecretId', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'tcaptcha_secret_key', 'label' => '腾讯云 SecretKey', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['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],
|
||||
['key' => 'audit_bbs_posts', 'label' => '审核论坛帖子', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_bbs_plate', 'label' => '审核论坛板块', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'audit_bbs_term', 'label' => '审核论坛分类', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Extensions schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Extensions {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'extensions',
|
||||
'title' => '扩展&增强',
|
||||
'sections' => [
|
||||
'ip' => AKSRT_Admin_Schema_Extensions_IP::build(),
|
||||
'audit' => AKSRT_Admin_Schema_Extensions_Audit::build(),
|
||||
'push' => AKSRT_Admin_Schema_Extensions_Push::build(),
|
||||
'security' => AKSRT_Admin_Schema_Extensions_Security::build(),
|
||||
'docs-mode' => AKSRT_Admin_Schema_Extensions_Docs_Mode::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Basic section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Basic {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'basic',
|
||||
'title' => '基础设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'switch', 'title' => '功能开关', 'fields' => [
|
||||
['key' => 'bbs_s', 'label' => '启用论坛', 'type' => 'switch', 'default' => true, 'help' => '关闭后论坛页面将返回 404'],
|
||||
]],
|
||||
['key' => 'admin', 'title' => '论坛管理', 'fields' => [
|
||||
['key' => 'bbs_admin_users', 'label' => '论坛管理员', 'type' => 'user_select', 'multiple' => true, 'default' => [], 'help' => '拥有跨版块全部治理权限,绕过 ban'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Board_Page section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Board_Page {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'board_page',
|
||||
'title' => '版块页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'layout', 'title' => '版块页布局', 'fields' => [
|
||||
['key' => 'bbs_plate_top_info_s', 'label' => '版块信息卡', 'type' => 'switch', 'default' => true, 'help' => '版块页顶部显示版块详情卡片'],
|
||||
['key' => 'bbs_plate_tab', 'label' => '版块栏目', 'type' => 'group', 'default' => [
|
||||
['label' => '全部', 'sort' => 'date'],
|
||||
['label' => '推荐', 'sort' => 'featured'],
|
||||
['label' => '最新回复', 'sort' => 'last_reply'],
|
||||
['label' => '热门', 'sort' => 'hot'],
|
||||
['label' => '精华', 'sort' => 'essence'],
|
||||
]],
|
||||
['key' => 'bbs_plate_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 0, 'min' => 0],
|
||||
['key' => 'bbs_plate_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Credits section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Credits {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'credits',
|
||||
'title' => '积分设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'earn', 'title' => '积分奖励', 'fields' => [
|
||||
['key' => 'bbs_credits_post_new', 'label' => '发帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0],
|
||||
['key' => 'bbs_credits_plate_new', 'label' => '建版奖励', 'type' => 'number', 'default' => 10, 'min' => 0],
|
||||
['key' => 'bbs_credits_comment_new', 'label' => '回复奖励', 'type' => 'number', 'default' => 2, 'min' => 0],
|
||||
['key' => 'bbs_credits_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0],
|
||||
['key' => 'bbs_credits_score_extra', 'label' => '被加分', 'type' => 'number', 'default' => 3, 'min' => 0],
|
||||
['key' => 'bbs_credits_essence', 'label' => '被设精华', 'type' => 'number', 'default' => 10, 'min' => 0],
|
||||
['key' => 'bbs_credits_posts_hot', 'label' => '成为热帖', 'type' => 'number', 'default' => 5, 'min' => 0],
|
||||
['key' => 'bbs_credits_adopted', 'label' => '被采纳', 'type' => 'number', 'default' => 20, 'min' => 0],
|
||||
['key' => 'bbs_credits_daily_max', 'label' => '每日积分上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '0 = 不限'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Homepage section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Homepage {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'homepage',
|
||||
'title' => '首页设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'tabs', 'title' => '首页 Tab', 'fields' => [
|
||||
['key' => 'bbs_home_tab', 'label' => '首页栏目', 'type' => 'group', 'default' => [
|
||||
['type' => 'follow', 'label' => '关注'],
|
||||
['type' => 'synthesis', 'label' => '综合'],
|
||||
['type' => 'plate', 'label' => '版块'],
|
||||
], 'help' => '可拖拽排序,支持 follow / synthesis / plate / tabs 类型'],
|
||||
['key' => 'bbs_home_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 1, 'min' => 0],
|
||||
['key' => 'bbs_home_tab_quick_posts_s', 'label' => '快速发帖', 'type' => 'switch', 'default' => true, 'help' => '首页顶部显示快速发帖入口'],
|
||||
['key' => 'bbs_home_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Names section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Names {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'names',
|
||||
'title' => '名称定义',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display_names', 'title' => '名词自定义', 'fields' => [
|
||||
['key' => 'bbs_forum_display_name', 'label' => '论坛', 'type' => 'text', 'default' => '论坛'],
|
||||
['key' => 'bbs_plate_display_name', 'label' => '版块', 'type' => 'text', 'default' => '版块'],
|
||||
['key' => 'bbs_posts_display_name', 'label' => '帖子', 'type' => 'text', 'default' => '帖子'],
|
||||
['key' => 'bbs_topic_display_name', 'label' => '话题', 'type' => 'text', 'default' => '话题'],
|
||||
['key' => 'bbs_tag_display_name', 'label' => '标签', 'type' => 'text', 'default' => '标签'],
|
||||
['key' => 'bbs_comment_display_name', 'label' => '回复', 'type' => 'text', 'default' => '回复'],
|
||||
['key' => 'bbs_question_badge_name', 'label' => '提问徽章', 'type' => 'text', 'default' => '提问'],
|
||||
['key' => 'bbs_question_ok_badge_name', 'label' => '已解决徽章', 'type' => 'text', 'default' => '已解决'],
|
||||
['key' => 'bbs_cat_moderator_name', 'label' => '分区版主', 'type' => 'text', 'default' => '分区版主'],
|
||||
['key' => 'bbs_plate_author_name', 'label' => '超级版主', 'type' => 'text', 'default' => '超级版主'],
|
||||
['key' => 'bbs_plate_moderator_name', 'label' => '版主', 'type' => 'text', 'default' => '版主'],
|
||||
['key' => 'bbs_plate_follow_name', 'label' => '关注', 'type' => 'text', 'default' => '关注'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Other section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Other {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'other',
|
||||
'title' => '其它设置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'vote', 'title' => '投票', 'fields' => [
|
||||
['key' => 'bbs_vote_number_type', 'label' => '投票显示', 'type' => 'select', 'default' => 'percent', 'options' => [
|
||||
['label' => '百分比', 'value' => 'percent'],
|
||||
['label' => '票数', 'value' => 'number'],
|
||||
]],
|
||||
['key' => 'bbs_vote_max', 'label' => '选项上限', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 20],
|
||||
]],
|
||||
['key' => 'moderator_apply', 'title' => '版主申请', 'fields' => [
|
||||
['key' => 'bbs_apply_moderator_desc', 'label' => '申请说明', 'type' => 'textarea', 'default' => '欢迎有热情的用户申请成为版主,协助管理社区秩序。'],
|
||||
]],
|
||||
['key' => 'misc', 'title' => '杂项', 'fields' => [
|
||||
['key' => 'bbs_term_thumb', 'label' => '标签默认缩略图', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'bbs_post_lists_hide_pay_s', 'label' => '列表隐藏付费标记', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_post_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 100, 'help' => '最小 5,最大 100'],
|
||||
['key' => 'bbs_t_placeholder', 'label' => '标题占位符', 'type' => 'text', 'default' => '请输入标题'],
|
||||
['key' => 'bbs_c_placeholder', 'label' => '内容占位符', 'type' => 'text', 'default' => '请输入内容'],
|
||||
]],
|
||||
['key' => 'url', 'title' => 'URL / 别名', 'fields' => [
|
||||
['key' => 'bbs_rewrite_suffix_html_s', 'label' => '.html 后缀', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_plate_rewrite_slug', 'label' => '版块别名', 'type' => 'text', 'default' => 'board'],
|
||||
['key' => 'bbs_posts_rewrite_slug', 'label' => '帖子别名', 'type' => 'text', 'default' => 'topic'],
|
||||
['key' => 'bbs_posts_edit_rewrite_slug', 'label' => '编辑别名', 'type' => 'text', 'default' => 'topic-edit'],
|
||||
['key' => 'bbs_posts_show_in_rest_s', 'label' => '古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '开启后帖子可使用 Gutenberg'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Permissions section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Permissions {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'permissions',
|
||||
'title' => '权限配置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'posting', 'title' => '发帖限制', 'fields' => [
|
||||
['key' => 'bbs_posts_add_limit_opt_max', 'label' => '每日发帖上限', 'type' => 'number', 'default' => 10, 'min' => 0, 'help' => '0 = 不限'],
|
||||
['key' => 'bbs_plate_add_limit_opt_max', 'label' => '建版数量上限', 'type' => 'number', 'default' => 3, 'min' => 0],
|
||||
]],
|
||||
['key' => 'display_btns', 'title' => '按钮显示', 'fields' => [
|
||||
['key' => 'bbs_show_new_plate', 'label' => '始终显示建版按钮', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_show_new_topic', 'label' => '始终显示发帖按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_show_apply_moderator', 'label' => '始终显示申请版主', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'paid_content', 'title' => '帖子付费', 'fields' => [
|
||||
['key' => 'bbs_post_pay_hide_type_s', 'label' => '启用付费内容', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_post_pay_vip_price_s', 'label' => 'VIP 特价', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_post_pay_vip_1_discount', 'label' => '一级 VIP 折扣 (%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100],
|
||||
['key' => 'bbs_post_pay_vip_2_discount', 'label' => '二级 VIP 折扣 (%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100],
|
||||
['key' => 'bbs_post_pay_price_limit', 'label' => '最高定价', 'type' => 'number', 'default' => 1000, 'min' => 1],
|
||||
]],
|
||||
['key' => 'plate_follow', 'title' => '版块付费关注', 'fields' => [
|
||||
['key' => 'bbs_plate_follow_pay_s', 'label' => '启用付费关注', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_plate_follow_pay_price', 'label' => '关注价格', 'type' => 'number', 'default' => 10, 'min' => 0],
|
||||
['key' => 'bbs_plate_follow_pay_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'money', 'options' => [
|
||||
['label' => '现金', 'value' => 'money'],
|
||||
['label' => '积分', 'value' => 'points'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Post_Page section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Post_Page {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'post_page',
|
||||
'title' => '帖子页面',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
|
||||
['key' => 'bbs_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_breadcrumbs_home', 'label' => '显示首页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_breadcrumbs_bbs_home', 'label' => '显示论坛首页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_breadcrumbs_bbs_home_name', 'label' => '论坛首页名称', 'type' => 'text', 'default' => '论坛'],
|
||||
['key' => 'bbs_breadcrumbs_plate_cat', 'label' => '显示分区', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'score', 'title' => '评分设置', 'fields' => [
|
||||
['key' => 'bbs_score_extra_max', 'label' => '加分上限', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
|
||||
['key' => 'bbs_score_deduct_max', 'label' => '扣分上限', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 50],
|
||||
]],
|
||||
['key' => 'display', 'title' => '显示', 'fields' => [
|
||||
['key' => 'bbs_posts_maxheight_s', 'label' => '内容高度限制', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_posts_maxheight', 'label' => '最大高度 (px)', 'type' => 'number', 'default' => 800, 'min' => 300, 'max' => 3000, 'condition' => ['bbs_posts_maxheight_s' => true]],
|
||||
]],
|
||||
['key' => 'cover', 'title' => '封面设置', 'fields' => [
|
||||
['key' => 'bbs_posts_cover_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' => '3:4', 'value' => '3:4'],
|
||||
]],
|
||||
['key' => 'bbs_posts_cover_video_mute', 'label' => '视频封面静音', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Reply section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Reply {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'reply',
|
||||
'title' => '回复评论',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'paginate', 'title' => '翻页', 'fields' => [
|
||||
['key' => 'bbs_reply_paginate_type', 'label' => '翻页方式', 'type' => 'select', 'default' => 'ajax', 'options' => [
|
||||
['label' => 'AJAX 加载', 'value' => 'ajax'],
|
||||
['label' => '传统翻页', 'value' => 'page'],
|
||||
]],
|
||||
['key' => 'bbs_reply_paginate_ias_s', 'label' => '无限滚动', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_reply_paginate_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10],
|
||||
]],
|
||||
['key' => 'toolbar', 'title' => '回复工具栏', 'fields' => [
|
||||
['key' => 'bbs_comment_smilie', 'label' => '表情', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_comment_code', 'label' => '代码', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_comment_img', 'label' => '插图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_comment_upload_img', 'label' => '上传图片', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'bbs_comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'bbs_comment_quick_often', 'label' => '预设短语', 'type' => 'textarea', 'default' => "不错,学习了\n感谢分享\n期待更新", 'help' => '每行一条'],
|
||||
['key' => 'bbs_comment_placeholder', 'label' => '回复占位符', 'type' => 'text', 'default' => '写下你的回复…'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum_Upload section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum_Upload {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'upload',
|
||||
'title' => '上传权限',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'limits', 'title' => '上传限制', 'fields' => [
|
||||
['key' => 'bbs_upload_img_size', 'label' => '图片大小 (KB)', 'type' => 'number', 'default' => 2048, 'min' => 100],
|
||||
['key' => 'bbs_image_upload_multiple', 'label' => '批量上传数', 'type' => 'number', 'default' => 9, 'min' => 1, 'max' => 20],
|
||||
['key' => 'bbs_upload_video_size', 'label' => '视频大小 (KB)', 'type' => 'number', 'default' => 20480, 'min' => 1024],
|
||||
['key' => 'bbs_upload_file_size', 'label' => '文件大小 (KB)', 'type' => 'number', 'default' => 10240, 'min' => 100],
|
||||
['key' => 'bbs_upload_file_mimes', 'label' => '允许格式', 'type' => 'text', 'default' => 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', 'help' => '逗号分隔'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Forum schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Forum {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'forum',
|
||||
'title' => '社区&论坛',
|
||||
'sections' => [
|
||||
'basic' => AKSRT_Admin_Schema_Forum_Basic::build(),
|
||||
'names' => AKSRT_Admin_Schema_Forum_Names::build(),
|
||||
'homepage' => AKSRT_Admin_Schema_Forum_Homepage::build(),
|
||||
'board_page' => AKSRT_Admin_Schema_Forum_Board_Page::build(),
|
||||
'post_page' => AKSRT_Admin_Schema_Forum_Post_Page::build(),
|
||||
'reply' => AKSRT_Admin_Schema_Forum_Reply::build(),
|
||||
'permissions' => AKSRT_Admin_Schema_Forum_Permissions::build(),
|
||||
'other' => AKSRT_Admin_Schema_Forum_Other::build(),
|
||||
'upload' => AKSRT_Admin_Schema_Forum_Upload::build(),
|
||||
'credits' => AKSRT_Admin_Schema_Forum_Credits::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §8.1 Capability matrix schema builder
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Build groups for the capabilities section from cap_definitions().
|
||||
*
|
||||
* Each capability becomes a group with role columns as fields.
|
||||
* Grouped by prefix: user_, new_post_, comment_.
|
||||
*/
|
||||
private static function build_cap_matrix_groups(): array {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$defs = AKSRT_Settings::cap_definitions();
|
||||
$cols = AKSRT_Settings::cap_role_columns();
|
||||
|
||||
$col_labels = [
|
||||
'all' => __('所有人', 'aksrt'),
|
||||
'logged' => __('已登录', 'aksrt'),
|
||||
'level' => __('等级≥', 'aksrt'),
|
||||
'vip' => __('VIP≥', 'aksrt'),
|
||||
'auth' => __('已认证', 'aksrt'),
|
||||
'moderator' => __('版主', 'aksrt'),
|
||||
'plate_author' => __('超级版主', 'aksrt'),
|
||||
'cat_moderator' => __('分区版主', 'aksrt'),
|
||||
];
|
||||
|
||||
// Group caps by prefix
|
||||
$buckets = [
|
||||
'user' => ['title' => __('用户治理', 'aksrt'), 'caps' => []],
|
||||
'post' => ['title' => __('投稿权限', 'aksrt'), 'caps' => []],
|
||||
'comment' => ['title' => __('评论权限', 'aksrt'), 'caps' => []],
|
||||
];
|
||||
|
||||
foreach ($defs as $cap_id => $def) {
|
||||
if (str_starts_with($cap_id, 'comment_')) {
|
||||
$buckets['comment']['caps'][$cap_id] = $def;
|
||||
} elseif (str_starts_with($cap_id, 'new_post_')) {
|
||||
$buckets['post']['caps'][$cap_id] = $def;
|
||||
} else {
|
||||
$buckets['user']['caps'][$cap_id] = $def;
|
||||
}
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
foreach ($buckets as $bucket_key => $bucket) {
|
||||
if (empty($bucket['caps'])) continue;
|
||||
|
||||
$fields = [];
|
||||
foreach ($bucket['caps'] as $cap_id => $def) {
|
||||
$excludes = $def['exclude_roles'] ?? [];
|
||||
$only_roles = $def['roles'] ?? null;
|
||||
$defaults = $def['default'] ?? [];
|
||||
|
||||
// Each cap gets one field per role column
|
||||
foreach ($cols as $col) {
|
||||
if (in_array($col, $excludes, true)) continue;
|
||||
if ($only_roles !== null && !in_array($col, $only_roles, true)
|
||||
&& !in_array($col, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_key = 'user_caps.' . $cap_id . '.' . $col;
|
||||
$is_spinner = in_array($col, ['level', 'vip'], true);
|
||||
|
||||
$field = [
|
||||
'key' => $field_key,
|
||||
'label' => $def['label'] . ' — ' . ($col_labels[$col] ?? $col),
|
||||
'type' => $is_spinner ? 'number' : 'switch',
|
||||
'default' => $defaults[$col] ?? ($is_spinner ? -1 : false),
|
||||
'cap_id' => $cap_id,
|
||||
'cap_col' => $col,
|
||||
];
|
||||
|
||||
if ($is_spinner) {
|
||||
$field['min'] = -1;
|
||||
$field['max'] = $col === 'level' ? 10 : 2;
|
||||
$field['help'] = __('-1 = 不限', 'aksrt');
|
||||
}
|
||||
|
||||
$fields[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$groups[] = [
|
||||
'key' => 'cap_' . $bucket_key,
|
||||
'title' => $bucket['title'],
|
||||
'fields' => $fields,
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Global schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Global {
|
||||
|
||||
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' => 'avatar', 'title' => '头像服务', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
]],
|
||||
]],
|
||||
['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_seo_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],
|
||||
['key' => 'seo_ai_enabled', 'label' => 'AI生成SEO内容', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'seo_connector', 'label' => 'SEO连接符', 'type' => 'input', 'default' => '-'],
|
||||
['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' => ''],
|
||||
['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['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', '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'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'avatar', 'title' => '头像设置', 'fields' => [
|
||||
['key' => 'avatar_source', 'label' => '头像服务源', 'type' => 'select', 'default' => 'cn.cravatar.com', 'options' => [
|
||||
['label' => 'Cravatar (国内推荐)', 'value' => 'cn.cravatar.com'],
|
||||
['label' => 'Gravatar 国内镜像', 'value' => 'cn.gravatar.com'],
|
||||
['label' => 'Gravatar 官方', 'value' => 'gravatar.com'],
|
||||
], 'help' => '国内站点建议优先选 Cravatar'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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],
|
||||
['key' => 'highlight_btn', 'label' => '显示扩展按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'highlight_maxheight', 'label' => '最大高度(px,0=不限)', 'type' => 'number', 'default' => 400, 'min' => 0, 'max' => 2000],
|
||||
]],
|
||||
['key' => 'theme', 'title' => '主题设置', 'fields' => [
|
||||
['key' => 'highlight_zt', 'label' => '日间默认主题', 'type' => 'select', 'default' => 'enlighter', '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', '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' => false, 'help' => '在搜索面板中显示热门文章列表'],
|
||||
['key' => 'search_no_page', 'label' => '排除页面', 'type' => 'switch', 'default' => false, 'help' => '搜索结果中排除页面(仅显示文章)'],
|
||||
['key' => 'search_exclude_cats', 'label' => '排除分类', 'type' => 'textarea', 'default' => '', 'help' => '搜索结果中排除的分类 ID(逗号分隔)'],
|
||||
['key' => 'search_custom_key_s', 'label' => '自定义匹配范围', 'type' => 'switch', 'default' => false, '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' => false, '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,530 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Page_Display schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Page_Display {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'page_display',
|
||||
'title' => '页面&显示',
|
||||
'sections' => [
|
||||
'header-nav' => [
|
||||
'key' => 'header-nav',
|
||||
'title' => '顶部导航栏',
|
||||
'version' => '1.0.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' => false],
|
||||
]],
|
||||
['key' => 'components', 'title' => '导航组件', 'fields' => [
|
||||
['key' => 'header_search_enabled', 'label' => '搜索入口', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_user_menu', 'label' => '用户菜单', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_publish_btn', 'label' => '发布按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_mobile_drawer', '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' => '有新文章'],
|
||||
['key' => 'header_newposts_radius', 'label' => '圆角', 'type' => 'number', 'default' => 12, 'min' => 0, 'max' => 30],
|
||||
['key' => 'header_newposts_class', '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' => ''],
|
||||
]],
|
||||
['key' => 'layout-type', 'title' => '布局类型', 'fields' => [
|
||||
['key' => 'header_layout', 'label' => '桌面端布局', 'type' => 'radio', 'default' => '2', 'options' => [
|
||||
['label' => '布局 1(经典)', 'value' => '1'],
|
||||
['label' => '布局 2(内联搜索)', 'value' => '2'],
|
||||
['label' => '布局 3(紧凑)', 'value' => '3'],
|
||||
]],
|
||||
['key' => 'mobile_header_layout', 'label' => '移动端布局', 'type' => 'radio', 'default' => 'center', 'options' => [
|
||||
['label' => '居中(含搜索按钮)', 'value' => 'center'],
|
||||
['label' => '靠左', 'value' => 'left'],
|
||||
]],
|
||||
['key' => 'mobile_navbar_align', 'label' => '移动端菜单方向', 'type' => 'radio', 'default' => 'left', 'options' => [
|
||||
['label' => '顶部(关闭手势)', 'value' => 'top'],
|
||||
['label' => '左侧滑出', 'value' => 'left'],
|
||||
['label' => '右侧滑出', 'value' => 'right'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'header-components' => [
|
||||
'key' => 'header-components',
|
||||
'title' => '顶部多功能组件',
|
||||
'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' => 'single'],
|
||||
['label' => '页面', 'value' => 'page'],
|
||||
['label' => '分类', 'value' => 'category'],
|
||||
['label' => '标签', 'value' => 'tag'],
|
||||
['label' => '搜索', 'value' => 'search'],
|
||||
['label' => '归档', 'value' => 'archive'],
|
||||
['label' => '专题', 'value' => 'special'],
|
||||
]],
|
||||
['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', 'options' => [
|
||||
['label' => '水平', 'value' => 'horizontal'],
|
||||
['label' => '垂直', 'value' => 'vertical'],
|
||||
]],
|
||||
['key' => 'slider_config_speed', 'label' => '切换速度(ms)', 'type' => 'number', 'default' => 300, 'min' => 100, 'max' => 3000],
|
||||
['key' => 'slider_config_button', 'label' => '显示前进后退按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'slider_config_scale_height', 'label' => '缩放高度', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'slider_config_scale', 'label' => '缩放效果', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'slider_config_max_height', 'label' => '最大高度', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'slider_config_min_height', 'label' => '最小高度', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'search', 'title' => '搜索组件', 'fields' => [
|
||||
['key' => 'header_components_search_pc', 'label' => 'PC 端显示搜索', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_components_search_mobile', 'label' => '移动端显示搜索', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_components_search_show_keywords', 'label' => '显示热门搜索词', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'header_components_search_popular_limit', 'label' => '热词数量', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
|
||||
['key' => 'header_components_search_show_input_cat', 'label' => '显示分类搜索', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'header_components_search_show_type', 'label' => '显示类型切换', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'cards', 'title' => '图标卡片样式', 'fields' => [
|
||||
['key' => 'header_components_filter_blur_devices', 'label' => '容器模糊设备', 'type' => 'checkbox', 'default' => [], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'header_components_card_show_devices', 'label' => '卡片显示设备', 'type' => 'checkbox', 'default' => ['pc', 'mobile'], 'options' => [
|
||||
['label' => 'PC', 'value' => 'pc'],
|
||||
['label' => '移动端', 'value' => 'mobile'],
|
||||
]],
|
||||
['key' => 'header_components_card_shared_bg', 'label' => '共享卡片背景', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'header_components_card_blur', 'label' => '启用卡片模糊效果', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'header_components_card_icon_shape', 'label' => '图标形状', 'type' => 'radio', 'default' => 'circle', 'options' => [
|
||||
['label' => '圆形', 'value' => 'circle'],
|
||||
['label' => '方形', 'value' => 'square'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'icon-cards', 'title' => '图标卡片列表', 'fields' => [
|
||||
['key' => 'header_components_icon_cards', 'label' => '图标卡片', 'type' => 'repeater', 'sanitize_type' => 'array', 'default' => []],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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],
|
||||
['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.0.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' => true],
|
||||
['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,comments,favorites,credits,product,plate,forum,follow'],
|
||||
['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,comments,favorites,credits,product,plate,forum,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' => true],
|
||||
['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' => '联系区上方自定义代码', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'fcode_t2_code_2', 'label' => '联系区下方自定义代码(备案等)', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['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按钮类型'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'float-btn' => [
|
||||
'key' => 'float-btn',
|
||||
'title' => '悬浮按钮',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '悬浮按钮设置', 'fields' => [
|
||||
['key' => 'float_btn_style', 'label' => '按钮风格', 'type' => 'radio', 'default' => 'round', 'options' => [
|
||||
['label' => '圆形', 'value' => 'round'],
|
||||
['label' => '方形', 'value' => 'square'],
|
||||
]],
|
||||
['key' => 'float_btn_position', 'label' => '按钮位置', 'type' => 'radio', 'default' => 'bottom', 'options' => [
|
||||
['label' => '底部', 'value' => 'bottom'],
|
||||
['label' => '居中', 'value' => 'center'],
|
||||
]],
|
||||
['key' => 'float_btn_scroll_hide', 'label' => '滚动时隐藏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'float_btn_scroll_down_hide', 'label' => '下滑时隐藏', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'float_btn_max', 'label' => '最大显示数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 10],
|
||||
['key' => 'float_btn_size', 'label' => '按钮尺寸', 'type' => 'number', 'default' => 44, 'min' => 32, 'max' => 64],
|
||||
['key' => 'float_btn_spacing', 'label' => '按钮间距', 'type' => 'number', 'default' => 10, 'min' => 4, 'max' => 24],
|
||||
['key' => 'float_btn_shadow', 'label' => '显示阴影', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'float_btn_animation', 'label' => '悬停动画', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'float_btn_opacity', 'label' => '按钮透明度', 'type' => 'number', 'default' => 100, 'min' => 30, 'max' => 100],
|
||||
['key' => 'float_btn_active_color', 'label' => '激活颜色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'float_btn_bg_color', 'label' => '背景颜色', 'type' => 'color', 'default' => ''],
|
||||
['key' => 'float_btn_pc_s', 'label' => 'PC端显示', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'float_btn_m_s', 'label' => '移动端显示', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'float_btn_filter_css', 'label' => '过滤CSS', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Payment schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Payment {
|
||||
|
||||
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' => '角色白名单免扣下载', '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' => 'payment_paid_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||
['key' => 'payment_currency_symbol', 'label' => '货币符号', 'type' => 'input', 'default' => '¥'],
|
||||
['key' => 'payment_paid_show_user', 'label' => '显示购买用户', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_paid_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_paid_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
|
||||
['label' => '文章模块上', 'value' => 'box_top'],
|
||||
['label' => '内容顶', 'value' => 'top'],
|
||||
['label' => '内容底', 'value' => 'bottom'],
|
||||
['label' => '模块下', 'value' => 'box_bottom'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'download', 'title' => '下载设置', 'fields' => [
|
||||
['key' => 'payment_download_enable', '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'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'recharge' => [
|
||||
'key' => 'recharge',
|
||||
'title' => '余额充值',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '充值开关', 'fields' => [
|
||||
['key' => 'payment_recharge_enabled', 'label' => '启用余额充值', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_recharge_mode', 'label' => '充值模式', 'type' => 'select', 'default' => 'manual_review', 'options' => [
|
||||
['label' => '人工审核', 'value' => 'manual_review'],
|
||||
['label' => '自动到账', 'value' => 'auto'],
|
||||
]],
|
||||
['key' => 'payment_recharge_desc', 'label' => '充值说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_recharge_auto_to_balance', 'label' => '自动到账到余额', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'packages', 'title' => '充值套餐', 'fields' => [
|
||||
['key' => 'payment_recharge_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|入门包\n98|108|推荐包"],
|
||||
['key' => 'payment_recharge_help', 'label' => '帮助文案', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'product', 'title' => '充值产品', 'fields' => [
|
||||
['key' => 'payment_recharge_product', 'label' => '充值产品', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_recharge_price', 'label' => '充值价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_recharge_tag', 'label' => '价格标签', 'type' => 'input', 'default' => '元'],
|
||||
['key' => 'payment_recharge_tag_class', 'label' => '标签样式', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_recharge_custom_s', 'label' => '自定义金额', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_recharge_custom_limit', 'label' => '自定义金额限制', 'type' => 'number', 'default' => 9999, 'min' => 1, 'max' => 99999],
|
||||
]],
|
||||
['key' => 'transfer', 'title' => '余额转账', 'fields' => [
|
||||
['key' => 'payment_balance_transfer_s', 'label' => '启用余额转账', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_balance_transfer_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50],
|
||||
['key' => 'payment_balance_transfer_desc', 'label' => '转账说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'pass-charge', 'title' => '充值卡免扣', 'fields' => [
|
||||
['key' => 'payment_recharge_pass_charge_s', 'label' => '启用充值卡免扣', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_recharge_pass_charge_desc', 'label' => '免扣说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_recharge_pass_charge_only_password', 'label' => '仅密码免扣', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'exchange', 'title' => '积分兑换', 'fields' => [
|
||||
['key' => 'payment_recharge_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_recharge_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000],
|
||||
['key' => 'payment_recharge_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'vip' => [
|
||||
'key' => 'vip',
|
||||
'title' => 'VIP会员',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '会员基础', 'fields' => [
|
||||
['key' => 'payment_vip_enabled', 'label' => '启用会员结构', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_name', 'label' => '会员名称', 'type' => 'input', 'default' => 'VIP会员'],
|
||||
['key' => 'payment_vip_badge_text', 'label' => '会员徽标文案', 'type' => 'input', 'default' => 'VIP'],
|
||||
['key' => 'payment_vip_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'packages', 'title' => '会员套餐', 'fields' => [
|
||||
['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡"],
|
||||
['key' => 'payment_vip_help', 'label' => '权益说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||
['key' => 'payment_vip_download_limit', 'label' => '下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
|
||||
['key' => 'payment_vip_download_speed', 'label' => '下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_vip_discount', 'label' => '折扣(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
|
||||
['key' => 'payment_vip_pass_charge', 'label' => '积分充值免扣', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_pass_charge_desc', 'label' => '积分充值说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'renew', 'title' => '续费设置', 'fields' => [
|
||||
['key' => 'payment_vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_vip_renew_desc', 'label' => '续费说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'same', 'options' => [
|
||||
['label' => '相同价格', 'value' => 'same'],
|
||||
['label' => '折扣价格', 'value' => 'discount'],
|
||||
]],
|
||||
['key' => 'payment_vip_renew_discount', 'label' => '续费折扣(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
|
||||
['key' => 'payment_vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '升级设置', 'fields' => [
|
||||
['key' => 'payment_vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_vip_upgrade_product', 'label' => '升级产品', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'price', 'title' => '价格设置', 'fields' => [
|
||||
['key' => 'payment_vip_month_price', 'label' => '月卡价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_vip_year_price', 'label' => '年卡价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_vip_permanent_price', 'label' => '永久价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_vip_unit_price', 'label' => '单位价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'payment_vip_unit_tag', 'label' => '单位标签', 'type' => 'input', 'default' => '元'],
|
||||
['key' => 'payment_vip_jump_price', 'label' => '跳转价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'revenue' => [
|
||||
'key' => 'revenue',
|
||||
'title' => '创作分成',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '分成策略', 'fields' => [
|
||||
['key' => 'payment_revenue_enabled', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_revenue_share_ratio', 'label' => '作者分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 1, 'max' => 100],
|
||||
['key' => 'payment_revenue_ratio_vip1', 'label' => 'VIP1分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'payment_revenue_ratio_vip2', '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' => 'payment_revenue_desc_details', 'label' => '收益明细文案', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_revenue_desc_page', 'label' => '收益说明页', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'affiliate' => [
|
||||
'key' => 'affiliate',
|
||||
'title' => '推广返佣',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '返佣策略', 'fields' => [
|
||||
['key' => 'payment_affiliate_enabled', 'label' => '启用推广返佣', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_affiliate_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' => 'payment_affiliate_self_s', 'label' => '自购返佣', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_affiliate_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' => 'payment_affiliate_vip1_ratio', 'label' => 'VIP1比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'payment_affiliate_vip2_ratio', 'label' => 'VIP2比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
]],
|
||||
['key' => 'poster', 'title' => '推广海报', 'fields' => [
|
||||
['key' => 'payment_affiliate_poster_s', 'label' => '启用推广海报', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_affiliate_poster_img', 'label' => '海报背景图', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'payment_affiliate_poster_title', 'label' => '海报标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_affiliate_poster_desc', 'label' => '海报描述', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'text', 'title' => '文案设置', 'fields' => [
|
||||
['key' => 'payment_affiliate_text_desc', 'label' => '返佣说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_affiliate_text_details_title', 'label' => '明细标题', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_affiliate_text_details', 'label' => '明细说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_affiliate_text_discount', 'label' => '折扣文案', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'gateway' => [
|
||||
'key' => 'gateway',
|
||||
'title' => '收款接口',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'adapter', 'title' => '网关适配器', 'fields' => [
|
||||
['key' => 'payment_gateway_mode', 'label' => '运行模式', 'type' => 'string', 'default' => 'manual_review'],
|
||||
['key' => 'payment_gateway_test_enabled', 'label' => '启用测试网关', 'type' => 'bool', 'default' => true],
|
||||
['key' => 'payment_gateway_manual_review', 'label' => '保留人工复核', 'type' => 'bool', 'default' => true],
|
||||
['key' => 'payment_gateway_callback_secret', 'label' => '回调校验口令', 'type' => 'string', 'default' => ''],
|
||||
['key' => 'payment_gateway_help', 'label' => '运维说明', 'type' => 'string', 'default' => '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。'],
|
||||
]],
|
||||
['key' => 'gateways', 'title' => '支付网关', 'fields' => [
|
||||
['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_alipay_enabled', 'label' => '支付宝', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_paypal_enabled', 'label' => 'PayPal', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_xunhupay_enabled', 'label' => '虎皮椒', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_xhpay_enabled', 'label' => '小火付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_payjs_enabled', 'label' => 'PayJS', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_epay_enabled', 'label' => '易支付', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vmq_enabled', 'label' => 'V免签', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_codepay_enabled', 'label' => 'Codepay', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_stripe_enabled', 'label' => 'Stripe', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'wechat', 'title' => '微信支付配置', 'fields' => [
|
||||
['key' => 'wechat_merchantid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_jsapi', 'label' => 'JSAPI密钥', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_cert_path', 'label' => '证书路径', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_key_path', 'label' => '密钥路径', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'alipay', 'title' => '支付宝配置', 'fields' => [
|
||||
['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_publickey', 'label' => '支付宝公钥', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_return_url', 'label' => '同步回调地址', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_notify_url', 'label' => '异步回调地址', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'paypal', 'title' => 'PayPal配置', 'fields' => [
|
||||
['key' => 'paypal_client_id', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'paypal_client_secret', 'label' => 'Client Secret', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'paypal_currency', 'label' => '货币', 'type' => 'input', 'default' => 'USD'],
|
||||
['key' => 'paypal_sandbox', 'label' => '沙箱模式', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'xunhupay', 'title' => '虎皮椒配置', 'fields' => [
|
||||
['key' => 'xunhupay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'xunhupay_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'xhpay', 'title' => '小火付配置', 'fields' => [
|
||||
['key' => 'xhpay_mchid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'xhpay_alipay_v2', 'label' => '支付宝V2', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'xhpay_wx_h5', 'label' => '微信H5', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'payjs', 'title' => 'PayJS配置', 'fields' => [
|
||||
['key' => 'payjs_mchid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payjs_key', 'label' => '通信密钥', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'epay', 'title' => '易支付配置', 'fields' => [
|
||||
['key' => 'epay_apiurl', 'label' => '接口地址', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'epay_pid', 'label' => '商户ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'epay_key', 'label' => '商户密钥', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'stripe', 'title' => 'Stripe配置', 'fields' => [
|
||||
['key' => 'stripe_secret_key', 'label' => 'Secret Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'stripe_publishable_key', 'label' => 'Publishable Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'stripe_webhook_secret', 'label' => 'Webhook Secret', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'stripe_currency', 'label' => '货币', 'type' => 'input', 'default' => 'usd'],
|
||||
['key' => 'stripe_test_mode', 'label' => '测试模式', 'type' => 'switch', 'default' => 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' => 'payment_show_user', 'label' => '显示用户信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_show_paycount', 'label' => '显示购买次数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_no_logged_in', 'label' => '未登录提示', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'payment_cookie_day', 'label' => 'Cookie有效期(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 31],
|
||||
['key' => 'payment_no_logged_remind', 'label' => '未登录提醒文案', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_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' => 'payment_mark', 'label' => '付费标记', 'type' => 'input', 'default' => '付费'],
|
||||
['key' => 'payment_box_position', 'label' => '付费框位置', 'type' => 'radio', 'default' => 'top', 'options' => [
|
||||
['label' => '文章模块上', 'value' => 'box_top'],
|
||||
['label' => '内容顶', 'value' => 'top'],
|
||||
['label' => '内容底', 'value' => 'bottom'],
|
||||
['label' => '模块下', 'value' => 'box_bottom'],
|
||||
]],
|
||||
['key' => 'payment_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' => 'payment_service', 'label' => '客服设置', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_service_icon', 'label' => '客服图标', 'type' => 'icon', 'default' => ''],
|
||||
['key' => 'payment_service_value', 'label' => '客服内容', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_service_qq', 'label' => 'QQ客服', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'payment_service_wechat', 'label' => '微信客服', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'card-pass' => [
|
||||
'key' => 'card-pass',
|
||||
'title' => '充值卡',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '充值卡设置', 'fields' => [
|
||||
['key' => 'card_pass_enabled', 'label' => '启用充值卡', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'card_pass_desc', 'label' => '充值卡说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'card_pass_type', 'label' => '充值卡类型', 'type' => 'select', 'default' => 'balance', 'options' => [
|
||||
['label' => '余额卡', 'value' => 'balance'],
|
||||
['label' => 'VIP卡', 'value' => 'vip'],
|
||||
['label' => '积分卡', 'value' => 'credits'],
|
||||
]],
|
||||
['key' => 'card_pass_prefix', 'label' => '卡号前缀', 'type' => 'input', 'default' => 'AK'],
|
||||
['key' => 'card_pass_length', 'label' => '卡号长度', 'type' => 'number', 'default' => 16, 'min' => 8, 'max' => 32],
|
||||
['key' => 'card_pass_batch_size', 'label' => '批量生成数量', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500],
|
||||
]],
|
||||
['key' => 'redeem', 'title' => '兑换设置', 'fields' => [
|
||||
['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100],
|
||||
['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
|
||||
['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 9999],
|
||||
['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650],
|
||||
['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'card_pass_redeem_page', 'label' => '兑换页面', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'card_pass_success_msg', 'label' => '兑换成功提示', 'type' => 'input', 'default' => '兑换成功!'],
|
||||
['key' => 'card_pass_fail_msg', 'label' => '兑换失败提示', 'type' => 'input', 'default' => '卡号无效或已过期'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Permissions schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Permissions {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'permissions',
|
||||
'title' => '功能&权限',
|
||||
'sections' => [
|
||||
'login' => [
|
||||
'key' => 'login',
|
||||
'title' => '登录权限',
|
||||
'version' => '1.0.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' => '', 'options' => [
|
||||
['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', 'options' => [
|
||||
['label' => '始终显示', 'value' => 'always'],
|
||||
]],
|
||||
['key' => 'disabled_scenarios', 'label' => '停用场景', 'type' => 'checkbox', 'default' => [], 'options' => [
|
||||
['label' => '发表评论', 'value' => 'comment'],
|
||||
['label' => '用户登录', 'value' => 'login'],
|
||||
['label' => '用户注册', 'value' => 'register'],
|
||||
['label' => '绑定第三方', 'value' => 'bind'],
|
||||
['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],
|
||||
['key' => 'download_credits_cost', 'label' => '下载所需积分', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100],
|
||||
['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', 'label' => '下载需要验证码', 'type' => 'switch', 'default' => false],
|
||||
['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' => 'enabled'],
|
||||
['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' => 'enabled'],
|
||||
['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' => 'enabled'],
|
||||
]],
|
||||
['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [
|
||||
['key' => 'rate_limit_enabled', 'label' => '启用频率限制', 'type' => 'switch', 'default' => false, 'dependsOn' => 'enabled'],
|
||||
['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' => 'enabled'],
|
||||
['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',
|
||||
'groups' => self::build_cap_matrix_groups(),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Post_List schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Post_List {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'post_list',
|
||||
'title' => '文章&列表',
|
||||
'sections' => [
|
||||
'list' => [
|
||||
'key' => 'list',
|
||||
'title' => '文章列表',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'card', 'title' => '卡片样式', 'fields' => [
|
||||
['key' => 'post_card_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'standard', 'options' => [
|
||||
['label' => '标准卡', 'value' => 'standard'],
|
||||
['label' => '紧凑卡', 'value' => 'compact'],
|
||||
['label' => '大图卡', 'value' => 'large_thumb'],
|
||||
['label' => '多图卡', 'value' => 'multi_thumb'],
|
||||
['label' => '纯文字', 'value' => 'text'],
|
||||
['label' => '有图则图文', 'value' => 'thumb_if_has'],
|
||||
]],
|
||||
['key' => 'post_excerpt_length', 'label' => '摘要长度', 'type' => 'number', 'default' => 120, 'min' => 20, 'max' => 500],
|
||||
]],
|
||||
['key' => 'meta', 'title' => '元信息显示', 'fields' => [
|
||||
['key' => 'post_show_author', 'label' => '显示作者', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_show_date', 'label' => '显示日期', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_show_views', 'label' => '显示浏览数', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_show_category', 'label' => '显示分类', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_show_tags', 'label' => '显示标签', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_show_comments', 'label' => '显示评论数', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_show_likes', 'label' => '显示点赞数', 'type' => 'switch', 'default' => false],
|
||||
['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' => 'post_list_meta_show', 'label' => '显示元信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'list_meta_show', 'label' => '移动端优先部件', 'type' => 'radio', 'default' => 'like', 'options' => [
|
||||
['label' => '浏览数', 'value' => 'view'],
|
||||
['label' => '点赞数', 'value' => 'like'],
|
||||
['label' => '评论数', 'value' => 'comm'],
|
||||
], 'description' => '移动端空间不足时保留显示的单一部件'],
|
||||
]],
|
||||
['key' => 'layout', 'title' => '布局设置', 'fields' => [
|
||||
['key' => 'post_list_columns', 'label' => '列表列数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 3],
|
||||
['key' => 'post_list_per_page', 'label' => '每页文章数', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 48],
|
||||
['key' => 'sticky_s', 'label' => '置顶范围', 'type' => 'checkbox', 'default' => ['home', 'cat'], 'options' => [
|
||||
['label' => '首页', 'value' => 'home'],
|
||||
['label' => '分类页', 'value' => 'cat'],
|
||||
['label' => '标签页', 'value' => 'tag'],
|
||||
['label' => '专题页', 'value' => 'topics'],
|
||||
]],
|
||||
['key' => 'post_target_blank', 'label' => '新窗口打开', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'item_heading_bold', 'label' => '标题加粗', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'list_show_type', 'label' => '列表样式', 'type' => 'radio', 'default' => 'separate', 'options' => [
|
||||
['label' => '分隔卡片', 'value' => 'separate'],
|
||||
['label' => '无间距', 'value' => 'no_margin'],
|
||||
]],
|
||||
['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],
|
||||
]],
|
||||
['key' => 'pagination', 'title' => '分页方式', 'fields' => [
|
||||
['key' => 'post_pagination_mode', 'label' => '分页模式', 'type' => 'radio', 'default' => 'numbered', 'options' => [
|
||||
['label' => '数字分页', 'value' => 'numbered'],
|
||||
['label' => '加载更多', 'value' => 'load_more'],
|
||||
['label' => '无限滚动', 'value' => 'infinite'],
|
||||
]],
|
||||
['key' => 'ias_max', 'label' => '最大加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10, 'description' => '0=不限'],
|
||||
['key' => 'ajax_trigger_text', 'label' => '加载更多文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'ajax_nomore_text', 'label' => '没有更多文案', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'sort', 'title' => '默认排序', 'fields' => [
|
||||
['key' => 'list_orderby', 'label' => '列表默认排序', 'type' => 'radio', 'default' => 'modified', 'options' => [
|
||||
['label' => '更新时间', 'value' => 'modified'],
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '评论数', 'value' => 'comments'],
|
||||
['label' => 'WordPress 默认', 'value' => 'default'],
|
||||
], 'help' => '分类/标签/归档页面的默认排序方式'],
|
||||
]],
|
||||
['key' => 'list_option', 'title' => '列表模式配置', 'fields' => [
|
||||
['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' => 'card_option', 'title' => '卡片模式配置', 'fields' => [
|
||||
['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],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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' => [], '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, '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', '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],
|
||||
['key' => 'mult_thumb_cat', 'label' => '按分类多图显示', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
|
||||
['key' => 'thumb_lazy', 'label' => '懒加载', 'type' => 'switch', 'default' => 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' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true],
|
||||
['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_type', 'label' => '推荐方式', 'type' => 'radio', 'default' => 'category', 'options' => [
|
||||
['label' => '同分类', 'value' => 'category'],
|
||||
['label' => '同标签', 'value' => 'tag'],
|
||||
['label' => '随机', 'value' => 'random'],
|
||||
]],
|
||||
['key' => 'post_related_orderby', 'label' => '推荐排序', 'type' => 'select', 'default' => 'rand', 'options' => [
|
||||
['label' => '随机', 'value' => 'rand'],
|
||||
['label' => '相关度', 'value' => 'relevance'],
|
||||
['label' => '时间', 'value' => 'date'],
|
||||
]],
|
||||
['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_enabled', 'label' => '打赏功能', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''],
|
||||
['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],
|
||||
['key' => 'filter_cat_show_tag', 'label' => '分类页:显示标签筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'filter_tag_show_cat', 'label' => '标签页:显示分类筛选', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'filter_tag_show_tag', 'label' => '标签页:显示标签筛选', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'filter_topics_show_cat', 'label' => '专题页:显示分类筛选', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'filter_topics_show_tag', 'label' => '专题页:显示标签筛选', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'custom_filter', 'title' => '自定义筛选维度', 'fields' => [
|
||||
['key' => 'custom_filter', 'label' => '筛选维度定义', 'type' => 'repeater', 'default' => [], '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' => [], 'help' => '为指定分类绑定不同的筛选维度组合', 'fields' => [
|
||||
['key' => 'cats', 'label' => '绑定分类', 'type' => 'taxonomy_select', 'taxonomy' => 'category', 'multiple' => true, 'default' => []],
|
||||
['key' => 'cat_s', 'label' => '显示分类筛选', 'type' => 'switch', 'default' => true],
|
||||
['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_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_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' => 'post_article_user', 'label' => '免登录归属用户', 'type' => 'user_select', 'default' => 0, '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,37 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Basic section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Basic {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'basic',
|
||||
'title' => '商城基础',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'general', 'title' => '商城开关', 'fields' => [
|
||||
['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'shop_home_url', 'label' => '商城首页链接', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
|
||||
['key' => 'shop_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'shop_breadcrumbs_home', 'label' => '显示首页链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'shop_breadcrumbs_shop_home', 'label' => '显示商城首页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'shop_breadcrumbs_shop_home_name', 'label' => '商城首页名称', 'type' => 'input', 'default' => '商城首页'],
|
||||
]],
|
||||
['key' => 'rewrite', 'title' => 'URL 别名', 'fields' => [
|
||||
['key' => 'shop_product_rewrite_slug', 'label' => '商品 URL 别名', 'type' => 'input', 'default' => 'product'],
|
||||
['key' => 'shop_cart_rewrite_slug', 'label' => '购物车 URL 别名', 'type' => 'input', 'default' => 'cart'],
|
||||
['key' => 'shop_rewrite_suffix_html_s', 'label' => '.html 伪静态后缀', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Contact section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Contact {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'contact',
|
||||
'title' => '客服配置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'button', 'title' => '客服按钮', 'fields' => [
|
||||
['key' => 'shop_contact_msg_s', 'label' => '显示客服按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'shop_contact_name', 'label' => '按钮文案', 'type' => 'input', 'default' => '联系客服'],
|
||||
['key' => 'shop_contact_note', 'label' => '客服备注', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'channels', 'title' => '联系方式', 'fields' => [
|
||||
['key' => 'shop_contact_more', 'label' => '多客服联系方式', 'type' => 'textarea', 'default' => '', 'help' => '每行一个:名称|图标|链接|二维码图片 URL'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_List section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_List {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'list',
|
||||
'title' => '商品列表',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'pagination', 'title' => '列表分页', 'fields' => [
|
||||
['key' => 'shop_list_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
|
||||
['label' => '最新发布', 'value' => 'date'],
|
||||
['label' => '销量最高', 'value' => 'sales'],
|
||||
['label' => '价格最低', 'value' => 'price_asc'],
|
||||
['label' => '价格最高', 'value' => 'price_desc'],
|
||||
['label' => '评价最多', 'value' => 'comments'],
|
||||
]],
|
||||
['key' => 'shop_list_count', 'label' => '每页数量', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 100],
|
||||
['key' => 'shop_list_paging', 'label' => '翻页模式', 'type' => 'radio', 'default' => 'number', 'options' => [
|
||||
['label' => '数字翻页', 'value' => 'number'],
|
||||
['label' => 'AJAX 加载', 'value' => 'ajax'],
|
||||
]],
|
||||
['key' => 'shop_list_ias_s', 'label' => '滚动自动加载', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'shop_list_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
|
||||
]],
|
||||
['key' => 'style', 'title' => '列表样式', 'fields' => [
|
||||
['key' => 'shop_list_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '竖向大卡片', 'value' => 'card-lg'],
|
||||
['label' => '横向小卡片', 'value' => 'card-sm'],
|
||||
]],
|
||||
['key' => 'shop_sales_show', 'label' => '销量显示', 'type' => 'select', 'default' => 'show', 'options' => [
|
||||
['label' => '显示', 'value' => 'show'],
|
||||
['label' => '不显示', 'value' => 'hide'],
|
||||
['label' => '超量显示', 'value' => 'threshold'],
|
||||
]],
|
||||
['key' => 'shop_sales_show_min', 'label' => '起显阈值', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 99999],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Product_Defaults section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Product_Defaults {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'product-defaults',
|
||||
'title' => '商品默认参数',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '购买策略', 'fields' => [
|
||||
['key' => 'shop_limit_buy', 'label' => '默认限购', 'type' => 'number', 'default' => -1, 'min' => -1, 'max' => 9999, 'help' => '-1 不限,0 禁购,正数为限购数量'],
|
||||
['key' => 'shop_guest_buy', 'label' => '免登录购买', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'shop_email_fill', 'label' => '邮箱必留', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'shop_rebate', 'label' => '允许返佣', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'shipping', 'title' => '运费默认', 'fields' => [
|
||||
['key' => 'shop_shipping_fee', 'label' => '默认运费类型', 'type' => 'select', 'default' => 'free', 'options' => [
|
||||
['label' => '包邮', 'value' => 'free'],
|
||||
['label' => '固定运费', 'value' => 'fixed'],
|
||||
['label' => '按金额计算', 'value' => 'amount'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'afterSale', 'title' => '售后与服务', 'fields' => [
|
||||
['key' => 'shop_after_sale', 'label' => '默认售后政策', 'type' => 'select', 'default' => 'refund_return', 'options' => [
|
||||
['label' => '仅退款', 'value' => 'refund'],
|
||||
['label' => '退货退款', 'value' => 'refund_return'],
|
||||
['label' => '不支持售后', 'value' => 'none'],
|
||||
]],
|
||||
['key' => 'shop_service', 'label' => '服务保障', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'params', 'title' => '默认参数模板', 'fields' => [
|
||||
['key' => 'shop_product_params_default', 'label' => '商品参数模板', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Shipping_Config section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Shipping_Config {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'shipping-config',
|
||||
'title' => '物流配置',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'address', 'title' => '收货地址', 'fields' => [
|
||||
['key' => 'shop_manual_address', 'label' => '手动填写收货地址', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'express', 'title' => '快递查询', 'fields' => [
|
||||
['key' => 'shop_express_api_sdk', 'label' => '快递查询接口', 'type' => 'select', 'default' => 'kuaidi100', 'options' => [
|
||||
['label' => '快递100', 'value' => 'kuaidi100'],
|
||||
['label' => '阿里云', 'value' => 'aliyun'],
|
||||
['label' => '快递鸟', 'value' => 'kdniao'],
|
||||
]],
|
||||
['key' => 'shop_express_query_interval', 'label' => '查询刷新间隔(分钟)', 'type' => 'number', 'default' => 60, 'min' => 30, 'max' => 240],
|
||||
]],
|
||||
['key' => 'credentials', 'title' => '接口凭据', 'fields' => [
|
||||
['key' => 'shop_express_kdniao_opt', 'label' => '快递鸟凭据', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'shop_express_kuaidi100_opt', 'label' => '快递100凭据', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'shop_express_aliyun_opt', 'label' => '阿里云凭据', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'companies', 'title' => '物流公司', 'fields' => [
|
||||
['key' => 'shop_shipping_company', 'label' => '物流公司列表', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Single section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Single {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'single',
|
||||
'title' => '商品详情',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'layout', 'title' => '详情页布局', 'fields' => [
|
||||
['key' => 'shop_content_layout', 'label' => '内容布局', 'type' => 'select', 'default' => 'adapt', 'options' => [
|
||||
['label' => '适应宽度', 'value' => 'adapt'],
|
||||
['label' => '全屏', 'value' => 'full'],
|
||||
['label' => '适应宽度+侧栏', 'value' => 'sidebar'],
|
||||
]],
|
||||
['key' => 'shop_content_show_bg', 'label' => '详情背景盒子', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'shop_single_tab', 'label' => '自定义 TAB 栏目', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'shop_content_after', 'label' => '详情页底部内容', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'related', 'title' => '相关推荐', 'fields' => [
|
||||
['key' => 'shop_single_related_s', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'shop_single_related_opt', 'label' => '推荐配置', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'image', 'title' => '商品图片', 'fields' => [
|
||||
['key' => 'shop_main_image_default', 'label' => '默认商品主图', 'type' => 'media', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop_Timing section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop_Timing {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'timing',
|
||||
'title' => '时效与售后原因',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'receipt', 'title' => '确认收货', 'fields' => [
|
||||
['key' => 'shop_order_receipt_max_day', 'label' => '自动确认收货(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 60],
|
||||
]],
|
||||
['key' => 'comment', 'title' => '评价时效', 'fields' => [
|
||||
['key' => 'shop_comment_max_day', 'label' => '评价时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 60],
|
||||
['key' => 'shop_comment_placeholder', 'label' => '评价占位文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'shop_comment_img_num', 'label' => '评价图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 9],
|
||||
]],
|
||||
['key' => 'afterSale', 'title' => '售后退货', 'fields' => [
|
||||
['key' => 'shop_after_sale_return_express_max_day', 'label' => '退货快递时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 30],
|
||||
['key' => 'shop_after_sale_reason_refund', 'label' => '仅退款原因', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'shop_after_sale_reason_return', 'label' => '退货退款原因', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'author', 'title' => '商家信息', 'fields' => [
|
||||
['key' => 'shop_author_show', 'label' => '显示商家信息', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Shop schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Shop {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'shop',
|
||||
'title' => '商城&商品',
|
||||
'version' => '1.0.0',
|
||||
'sections' => [
|
||||
'basic' => AKSRT_Admin_Schema_Shop_Basic::build(),
|
||||
'list' => AKSRT_Admin_Schema_Shop_List::build(),
|
||||
'contact' => AKSRT_Admin_Schema_Shop_Contact::build(),
|
||||
'single' => AKSRT_Admin_Schema_Shop_Single::build(),
|
||||
'product-defaults' => AKSRT_Admin_Schema_Shop_Product_Defaults::build(),
|
||||
'shipping-config' => AKSRT_Admin_Schema_Shop_Shipping_Config::build(),
|
||||
'timing' => AKSRT_Admin_Schema_Shop_Timing::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools_Backup section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools_Backup {
|
||||
|
||||
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
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools_Email_Rollback section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools_Email_Rollback {
|
||||
|
||||
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
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools_Health section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools_Health {
|
||||
|
||||
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
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools_Import_Export section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools_Import_Export {
|
||||
|
||||
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,27 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools_Migration section schema.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools_Migration {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'migration',
|
||||
'title' => '数据迁移',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'status', 'title' => '迁移状态', 'fields' => [
|
||||
['key' => 'current_version', 'label' => '当前版本', 'type' => 'display'],
|
||||
['key' => 'pending_migrations', 'label' => '待执行迁移', 'type' => 'display'],
|
||||
['key' => 'run_migration', 'label' => '执行迁移', 'type' => 'button', 'help' => '手动触发数据库迁移'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Tools schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Tools {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'domain' => 'tools',
|
||||
'title' => '工具&诊断',
|
||||
'sections' => [
|
||||
'import-export' => AKSRT_Admin_Schema_Tools_Import_Export::build(),
|
||||
'health' => AKSRT_Admin_Schema_Tools_Health::build(),
|
||||
'migration' => AKSRT_Admin_Schema_Tools_Migration::build(),
|
||||
'backup' => AKSRT_Admin_Schema_Tools_Backup::build(),
|
||||
'email-rollback' => AKSRT_Admin_Schema_Tools_Email_Rollback::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT_Admin_Schema_Users schema builder.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Admin_Schema_Users {
|
||||
|
||||
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' => false, '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' => false],
|
||||
['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' => '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' => 'alipay', 'title' => '支付宝配置', 'fields' => [
|
||||
['key' => 'alipay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'alipay_privatekey', 'label' => '应用私钥', 'type' => 'input', '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', 'options' => [
|
||||
['label' => '阿里云短信', 'value' => 'aliyun'],
|
||||
['label' => '腾讯云短信', 'value' => 'tencent'],
|
||||
['label' => '华为云短信', 'value' => 'huawei'],
|
||||
['label' => '自定义接口', 'value' => 'custom'],
|
||||
]],
|
||||
['key' => 'sms_number_limit_ch', 'label' => '仅发国内', 'type' => 'switch', 'default' => true, 'desc' => '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信'],
|
||||
['key' => 'sms_access_key', 'label' => 'AccessKey / SecretId / AppKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_access_secret', 'label' => 'AccessSecret / SecretKey / AppSecret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'aliyun', 'title' => '阿里云配置', 'fields' => [
|
||||
['key' => 'sms_ali_api_type', 'label' => '接口类型', 'type' => 'radio', 'default' => 'sms', 'options' => [
|
||||
['label' => '短信服务', 'value' => 'sms'],
|
||||
['label' => '号码认证', 'value' => 'verify'],
|
||||
]],
|
||||
['key' => 'sms_region_id', 'label' => 'RegionId', 'type' => 'input', 'default' => 'cn-hangzhou'],
|
||||
]],
|
||||
['key' => 'tencent', 'title' => '腾讯云配置', 'fields' => [
|
||||
['key' => 'sms_tencent_app_id', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_tencent_app_key', 'label' => 'AppKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_tencent_secret_id', 'label' => 'SecretId', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_tencent_secret_key', 'label' => 'SecretKey', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_tencent_region', 'label' => '地域', 'type' => 'input', 'default' => 'ap-guangzhou'],
|
||||
]],
|
||||
['key' => 'huawei', 'title' => '华为云配置', 'fields' => [
|
||||
['key' => 'sms_huawei_sender', 'label' => '通道号 (sender)', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_huawei_signature', 'label' => '短信签名', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_huawei_endpoint', 'label' => 'API Endpoint', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'custom', 'title' => '自定义接口配置', 'fields' => [
|
||||
['key' => 'sms_custom_url', 'label' => '请求 URL', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_custom_method', 'label' => '请求方法', 'type' => 'select', 'default' => 'POST', 'options' => [
|
||||
['label' => 'POST', 'value' => 'POST'],
|
||||
['label' => 'GET', 'value' => 'GET'],
|
||||
['label' => 'PUT', 'value' => 'PUT'],
|
||||
]],
|
||||
['key' => 'sms_custom_auth_type', 'label' => '鉴权方式', 'type' => 'select', 'default' => 'header', 'options' => [
|
||||
['label' => 'Header', 'value' => 'header'],
|
||||
]],
|
||||
['key' => 'sms_custom_auth_key', 'label' => '鉴权 Header 名', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_custom_auth_value', 'label' => '鉴权 Header 值', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_custom_headers', 'label' => '自定义 Headers', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'sms_custom_body', 'label' => '请求 Body', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'sms_custom_success_rule', 'label' => '成功判定规则', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'template', 'title' => '短信模板', 'fields' => [
|
||||
['key' => 'sms_sign_name', 'label' => '短信签名', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_template_code', 'label' => '验证码模板ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'sms_code_length', 'label' => '验证码长度', 'type' => 'number', 'default' => 6, 'min' => 4, 'max' => 8],
|
||||
['key' => 'sms_code_expire', 'label' => '验证码有效期(秒)', 'type' => 'number', 'default' => 300, 'min' => 60, 'max' => 600],
|
||||
]],
|
||||
['key' => 'rate_limit', 'title' => '限流策略', 'fields' => [
|
||||
['key' => 'sms_rate_limit', 'label' => '发送间隔(秒)', 'type' => 'number', 'default' => 60, 'min' => 10, 'max' => 300],
|
||||
['key' => 'sms_daily_limit', 'label' => '每日发送上限', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100],
|
||||
]],
|
||||
['key' => 'triggers', 'title' => '通知场景', 'fields' => [
|
||||
['key' => 'sms_notify_bind', 'label' => '绑定手机验证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sms_notify_login', 'label' => '短信登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'sms_notify_register', 'label' => '注册验证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sms_notify_reset_password', 'label' => '找回密码', 'type' => 'switch', 'default' => 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_comment', 'label' => '评论奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_earn_post', 'label' => '发布文章奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_earn_login', 'label' => '每日登录奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_earn_like_received', 'label' => '收到点赞奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000],
|
||||
['key' => 'points_earn_daily_limit', 'label' => '每日获取上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 9999, 'help' => '0为不限制'],
|
||||
]],
|
||||
['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],
|
||||
['key' => 'points_deduction_rate', 'label' => '抵扣比例(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100],
|
||||
['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100],
|
||||
]],
|
||||
['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,posts,comments,credits,favorites'],
|
||||
['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true],
|
||||
['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' => '显示城市', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
||||
['key' => 'user_avatar_default', 'label' => '默认头像', 'type' => 'media', 'default' => ''],
|
||||
['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_level', '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],
|
||||
]],
|
||||
['key' => 'rewards', 'title' => '打赏设置', 'fields' => [
|
||||
['key' => 'post_rewards_s', 'label' => '启用文章打赏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'post_rewards_img', 'label' => '打赏二维码', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'post_rewards_btn_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'],
|
||||
['key' => 'post_rewards_amounts', 'label' => '打赏金额', 'type' => 'input', 'default' => '5,10,20,50,100'],
|
||||
['key' => 'post_rewards_custom', 'label' => '自定义金额', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'verify' => [
|
||||
'key' => 'verify',
|
||||
'title' => '身份认证',
|
||||
'version' => '1.0.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_badge_text', 'label' => '认证徽标文案', 'type' => 'input', 'default' => '已认证'],
|
||||
]],
|
||||
['key' => 'workflow', 'title' => '审核流程', 'fields' => [
|
||||
['key' => 'user_verify_accept_email_verified', 'label' => '邮箱验证通过视为已认证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_verify_apply_option', 'label' => '申请选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证"],
|
||||
]],
|
||||
['key' => 'restrict', 'title' => '限制设置', 'fields' => [
|
||||
['key' => 'user_verify_disable_desc', 'label' => '未认证限制说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'user_verify_limit_level', 'label' => '限制等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'user_verify_desc', 'label' => '认证说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'level' => [
|
||||
'key' => 'level',
|
||||
'title' => '用户等级',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
||||
['key' => 'user_level_enabled', 'label' => '启用等级系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
|
||||
['key' => 'user_level_color', 'label' => '等级颜色', 'type' => 'color', 'default' => '#f04494'],
|
||||
['key' => 'user_level_icon', 'label' => '等级图标', 'type' => 'icon', 'default' => 'fas fa-star'],
|
||||
]],
|
||||
['key' => 'rules', 'title' => '等级规则', 'fields' => [
|
||||
['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'credits_balance'],
|
||||
['key' => 'user_level_thresholds', 'label' => '等级阈值', 'type' => 'textarea', 'default' => "1|0\n2|30\n3|100\n4|300"],
|
||||
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 99],
|
||||
['key' => 'user_level_decay', 'label' => '等级衰减', 'type' => 'switch', 'default' => false, 'help' => '长期不活跃自动降级'],
|
||||
['key' => 'user_level_decay_days', 'label' => '衰减周期(天)', 'type' => 'number', 'default' => 90, 'min' => 7, 'max' => 365],
|
||||
]],
|
||||
['key' => 'privilege', 'title' => '等级特权', 'fields' => [
|
||||
['key' => 'user_level_download_limit', 'label' => '下载限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户每日下载次数增加'],
|
||||
['key' => 'user_level_upload_limit', 'label' => '上传限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户可上传更大文件'],
|
||||
['key' => 'user_level_comment_cooldown', 'label' => '评论冷却缩短', 'type' => 'switch', 'default' => false, 'help' => '高等级用户评论间隔更短'],
|
||||
['key' => 'user_level_custom_title', 'label' => '自定义头衔', 'type' => 'switch', 'default' => false, 'help' => '允许高等级用户自定义头衔'],
|
||||
]],
|
||||
['key' => 'notify', 'title' => '升级通知', 'fields' => [
|
||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_notify_style', 'label' => '通知方式', 'type' => 'select', 'default' => 'toast', 'options' => [
|
||||
['label' => '弹窗提示', 'value' => 'toast'],
|
||||
['label' => '站内信', 'value' => 'message'],
|
||||
['label' => '两者都有', 'value' => 'both'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'badge' => [
|
||||
'key' => 'badge',
|
||||
'title' => '用户徽章',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '徽章展示', 'fields' => [
|
||||
['key' => 'user_badge_enabled', 'label' => '启用徽章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_badge_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_badge_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
]],
|
||||
['key' => 'dictionary', 'title' => '徽章字典', 'fields' => [
|
||||
['key' => 'user_badge_dictionary', 'label' => '徽章列表', 'type' => 'textarea', 'default' => "founder|创始成员|warning\nstaff|站务团队|info"],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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],
|
||||
['key' => 'user_report_public_notice', 'label' => '公开显示限制提示', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_report_ban_notice_text', 'label' => '限制提示文案', 'type' => 'input', 'default' => '当前账号处于限制状态,部分互动能力已关闭。'],
|
||||
]],
|
||||
['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|违规操作"],
|
||||
['key' => 'ban_appeal_desc', 'label' => '申诉说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'ban_appeal_keys', 'label' => '申诉关键词', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'image', 'title' => '图片举报', 'fields' => [
|
||||
['key' => 'user_report_img_s', 'label' => '启用图片举报', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_report_desc', 'label' => '举报说明', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'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' => ''],
|
||||
['key' => 'wechat_app_secret', 'label' => 'AppSecret', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'verify', 'title' => '服务器验证', 'fields' => [
|
||||
['key' => 'wechat_token', 'label' => 'Token', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_encoding_aes_key', 'label' => 'EncodingAESKey', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'templates', 'title' => '模板消息', 'fields' => [
|
||||
['key' => 'wechat_template_comment', 'label' => '评论通知模板', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_template_reply', 'label' => '回复通知模板', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_template_system', 'label' => '系统通知模板', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'wechat_template_order', 'label' => '订单通知模板', 'type' => 'input', 'default' => ''],
|
||||
]],
|
||||
['key' => 'triggers', 'title' => '通知触发器', 'fields' => [
|
||||
['key' => 'wechat_notify_comment', 'label' => '新评论通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'wechat_notify_reply', 'label' => '评论回复通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'wechat_notify_system', 'label' => '系统通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'wechat_notify_order', 'label' => '订单通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'wechat_bind_entry', 'label' => '显示绑定入口', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Auth_Service_Part_1a {
|
||||
|
||||
|
||||
public static function register_user(array $post, array $context = []): array {
|
||||
// 先 reset pending invite context,避免同一进程内残留污染
|
||||
self::$pending_invite_inviter_id = 0;
|
||||
self::$pending_invite_code = '';
|
||||
|
||||
$user_login = sanitize_user(wp_unslash($post['user_login'] ?? ''), true);
|
||||
$user_email = sanitize_email(wp_unslash($post['user_email'] ?? ''));
|
||||
$password = (string) ($post['user_pass'] ?? '');
|
||||
$confirm = (string) ($post['user_pass_confirm'] ?? '');
|
||||
|
||||
if (!wp_verify_nonce($post['_register_nonce'] ?? '', 'aksrt_register')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
if (empty($post['agree_terms'])) {
|
||||
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
|
||||
}
|
||||
|
||||
if (empty($user_login) || !validate_username($user_login)) {
|
||||
return ['success' => false, 'error' => __('请输入有效的用户名', 'aksrt')];
|
||||
}
|
||||
|
||||
if (mb_strlen($user_login) < 3 || mb_strlen($user_login) > 60) {
|
||||
return ['success' => false, 'error' => __('用户名长度需在 3-60 个字符之间', 'aksrt')];
|
||||
}
|
||||
|
||||
if (username_exists($user_login)) {
|
||||
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
|
||||
}
|
||||
|
||||
if (empty($user_email) || !is_email($user_email)) {
|
||||
return ['success' => false, 'error' => __('请输入有效的邮箱地址', 'aksrt')];
|
||||
}
|
||||
|
||||
if (email_exists($user_email)) {
|
||||
return ['success' => false, 'error' => __('该邮箱已被其他账号使用', 'aksrt')];
|
||||
}
|
||||
|
||||
$password_check = self::validate_password($password, $confirm);
|
||||
if ($password_check !== true) {
|
||||
return ['success' => false, 'error' => $password_check];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('register')) {
|
||||
if (function_exists('aksrt_verify_captcha')) {
|
||||
$captcha_result = aksrt_verify_captcha($post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 解析邀请码 ======
|
||||
// 优先级:表单 invite_code(用户显式填写) > cookie aksrt_invite
|
||||
$invite_registration_enabled = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_registration_enabled();
|
||||
$invite_mode = $invite_registration_enabled && class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::invite_register_field_mode()
|
||||
: 'hidden';
|
||||
$invite_code = isset($post['invite_code']) ? sanitize_key((string) wp_unslash($post['invite_code'])) : '';
|
||||
$resolved_inviter_id = 0;
|
||||
$invite_is_valid = false;
|
||||
|
||||
if (!$invite_registration_enabled) {
|
||||
// 邀请功能关闭,忽略所有邀请码
|
||||
$invite_code = '';
|
||||
} elseif ($invite_code !== '') {
|
||||
if (class_exists('AKSRT_Invite')) {
|
||||
$invite_service = AKSRT_Invite::get_instance();
|
||||
$resolved_inviter_id = $invite_service->resolve_code($invite_code);
|
||||
$invite_is_valid = $invite_service->validate_code($invite_code);
|
||||
}
|
||||
// 必填模式下,邀请码无效时根据后台开关决定是否拒绝
|
||||
$reject_invalid = class_exists('AKSRT_Settings') && AKSRT_Settings::invite_reject_invalid();
|
||||
if ($invite_mode === 'required' && $reject_invalid && !$invite_is_valid) {
|
||||
return ['success' => false, 'error' => __('邀请码无效,请检查后重试', 'aksrt')];
|
||||
}
|
||||
} elseif ($invite_mode === 'required') {
|
||||
return ['success' => false, 'error' => __('请填写邀请码', 'aksrt')];
|
||||
}
|
||||
|
||||
// 把解析到的 inviter_id 暂存到静态字段,user_register hook 内部读取(优先于 cookie)
|
||||
self::$pending_invite_inviter_id = $resolved_inviter_id;
|
||||
self::$pending_invite_code = $invite_code;
|
||||
|
||||
$user_id = wp_create_user($user_login, $password, $user_email);
|
||||
if (is_wp_error($user_id)) {
|
||||
self::$pending_invite_inviter_id = 0;
|
||||
self::$pending_invite_code = '';
|
||||
return ['success' => false, 'error' => $user_id->get_error_message()];
|
||||
}
|
||||
|
||||
if ($invite_code !== '' && $resolved_inviter_id <= 0 && class_exists('AKSRT_Invite')) {
|
||||
AKSRT_Invite::get_instance()->handle_registration_invite((int) $user_id, $invite_code);
|
||||
}
|
||||
|
||||
// wp_create_user 内部已触发 user_register hook,AKSRT_Invite::on_user_register 已读取 pending 字段。
|
||||
// 这里清空,避免污染后续请求。
|
||||
self::$pending_invite_inviter_id = 0;
|
||||
self::$pending_invite_code = '';
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('注册成功,但读取用户信息失败,请联系管理员', 'aksrt')];
|
||||
}
|
||||
|
||||
$oauth_context = isset($context['oauth_bind_data']) && is_array($context['oauth_bind_data']) ? $context['oauth_bind_data'] : [];
|
||||
$oauth_verified = self::oauth_email_is_verified($oauth_context);
|
||||
$requires_verify = AKSRT_Settings::registration_requires_email_verification() && !$oauth_verified;
|
||||
|
||||
if (!$requires_verify) {
|
||||
self::mark_email_verified($user_id);
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => (int) $user_id,
|
||||
'user' => $user,
|
||||
'user_login' => $user_login,
|
||||
];
|
||||
}
|
||||
|
||||
$method = AKSRT_Settings::registration_verification_method();
|
||||
$begin = self::begin_email_verification(
|
||||
$user,
|
||||
$method,
|
||||
[
|
||||
'obk' => sanitize_text_field((string) ($context['obk'] ?? '')),
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($begin['success'])) {
|
||||
self::delete_user_account($user_id);
|
||||
return $begin;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => (int) $user_id,
|
||||
'user' => $user,
|
||||
'user_login' => $user_login,
|
||||
'verification_required' => true,
|
||||
'verification_method' => $method,
|
||||
'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
|
||||
'message' => $begin['message'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
public static function bind_oauth_account(array $post, array $oauth_data): array {
|
||||
if (!wp_verify_nonce($post['_bind_nonce'] ?? '', 'aksrt_bind')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('bind')) {
|
||||
if (function_exists('aksrt_verify_captcha')) {
|
||||
$captcha_result = aksrt_verify_captcha($post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$creds = [
|
||||
'user_login' => self::resolve_email_to_username((string) ($post['log'] ?? '')),
|
||||
'user_password' => (string) ($post['pwd'] ?? ''),
|
||||
'remember' => true,
|
||||
];
|
||||
|
||||
$user = wp_signon($creds);
|
||||
if (is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
|
||||
}
|
||||
|
||||
if (self::user_requires_email_verification($user)) {
|
||||
wp_logout();
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => __('该账号尚未完成邮箱验证,请先完成验证后再绑定第三方登录。', 'aksrt'),
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
];
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
|
||||
if ($provider === '') {
|
||||
return ['success' => false, 'error' => __('绑定数据无效,请重新使用第三方登录', 'aksrt')];
|
||||
}
|
||||
|
||||
$uid = (int) $user->ID;
|
||||
update_user_meta($uid, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
|
||||
|
||||
$bindings = get_user_meta($uid, 'aksrt_oauth_bindings', true) ?: [];
|
||||
if (!is_array($bindings)) {
|
||||
$bindings = [];
|
||||
}
|
||||
if (!in_array($provider, $bindings, true)) {
|
||||
$bindings[] = $provider;
|
||||
}
|
||||
update_user_meta($uid, 'aksrt_oauth_bindings', $bindings);
|
||||
|
||||
if (!empty($oauth_data['user_data']['avatar_url'])) {
|
||||
update_user_meta($uid, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
|
||||
}
|
||||
|
||||
if (!empty($post['obk'])) {
|
||||
delete_transient(sanitize_text_field((string) $post['obk']));
|
||||
}
|
||||
|
||||
wp_set_auth_cookie($uid, true);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user' => $user,
|
||||
'provider' => $provider,
|
||||
];
|
||||
}
|
||||
|
||||
public static function finalize_oauth_registration(int $user_id, array $oauth_data, string $obk = ''): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
|
||||
if ($provider === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
update_user_meta($user_id, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
|
||||
update_user_meta($user_id, 'aksrt_oauth_bindings', [$provider]);
|
||||
|
||||
if (!empty($oauth_data['user_data']['avatar_url'])) {
|
||||
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
|
||||
}
|
||||
|
||||
if (self::oauth_email_is_verified($oauth_data)) {
|
||||
AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
|
||||
}
|
||||
|
||||
if ($obk !== '') {
|
||||
delete_transient(sanitize_text_field($obk));
|
||||
}
|
||||
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
}
|
||||
|
||||
public static function get_email_verification_context(array $query): array {
|
||||
$invalid_payload = [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt'),
|
||||
'method' => '',
|
||||
'email' => '',
|
||||
'uid' => 0,
|
||||
'verified' => false,
|
||||
'access_key' => '',
|
||||
];
|
||||
|
||||
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
|
||||
if ($user_id <= 0) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
$method = self::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
|
||||
$token = sanitize_text_field((string) ($query['token'] ?? ''));
|
||||
$access = sanitize_text_field((string) ($query['vk'] ?? ''));
|
||||
|
||||
// 容错:meta 缺失时(例如历史账号被强制要求验证),回退到设置默认值与 user_email
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = self::current_registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
$masked_email = self::masked_email($email);
|
||||
$has_access = self::has_valid_email_access_key($user_id, $access);
|
||||
|
||||
// token 路径:链接模式直接走 token 校验;不依赖 vk
|
||||
if ($token !== '') {
|
||||
if (AKSRT_Auth_Email_Verification::is_email_verified($user_id)) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => $user,
|
||||
'error' => __('该验证链接已失效或已使用,请重新发起验证流程', 'aksrt'),
|
||||
'method' => $has_access ? $method : '',
|
||||
'email' => $has_access ? $masked_email : '',
|
||||
'uid' => $has_access ? $user_id : 0,
|
||||
'verified' => true,
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
$result = self::verify_email_token($user_id, $token);
|
||||
if (!empty($result['success'])) {
|
||||
return [
|
||||
'mode' => 'completed',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => $method,
|
||||
'email' => $masked_email,
|
||||
'uid' => $user_id,
|
||||
'verified' => true,
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'message' => $result['message'] ?? '',
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => $user,
|
||||
'error' => $result['error'] ?? __('验证链接无效或已过期', 'aksrt'),
|
||||
'method' => $has_access ? $method : '',
|
||||
'email' => $has_access ? $masked_email : '',
|
||||
'uid' => $has_access ? $user_id : 0,
|
||||
'verified' => false,
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
// 没有 token 也没有 vk:返回完全脱敏的通用错误,避免根据 uid 枚举账号状态
|
||||
if (!$has_access) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
if (!self::user_requires_email_verification($user)) {
|
||||
return [
|
||||
'mode' => 'verified',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => '',
|
||||
'email' => self::masked_email((string) $user->user_email),
|
||||
'uid' => $user_id,
|
||||
'verified' => true,
|
||||
'access_key' => $access,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => $method === 'code' ? 'code' : 'pending',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => $method,
|
||||
'email' => $masked_email ?: self::masked_email((string) $user->user_email),
|
||||
'uid' => $user_id,
|
||||
'verified' => false,
|
||||
'access_key' => $access,
|
||||
];
|
||||
}
|
||||
|
||||
public static function verify_email_code_submission(array $post): array {
|
||||
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
|
||||
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['email_code'] ?? ''));
|
||||
if ($user_id <= 0 || $code === '') {
|
||||
return ['success' => false, 'error' => __('请输入完整的邮箱验证码', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!self::has_valid_email_access_key($user_id, $access)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) {
|
||||
if (function_exists('aksrt_verify_captcha')) {
|
||||
$captcha_result = aksrt_verify_captcha($post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::verify_email_code($user_id, $code);
|
||||
}
|
||||
|
||||
public static function resend_email_verification(array $post): array {
|
||||
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
|
||||
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
|
||||
|
||||
// 必须先持有有效 vk 才允许重发,避免未登录用户用 uid 枚举触发邮件
|
||||
if ($user_id <= 0 || !self::has_valid_email_access_key($user_id, $access)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
// IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次(跨账号合并计数)
|
||||
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($ip !== '') {
|
||||
$ip_key = 'aksrt_email_resend_ip_' . md5($ip);
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
if ($ip_attempts >= self::RESEND_IP_MAX) {
|
||||
self::log_event('verify.resend.ip_throttled', ['uid' => $user_id, 'ip' => $ip]);
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
set_transient($ip_key, $ip_attempts + 1, self::RESEND_IP_WINDOW);
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!self::user_requires_email_verification($user)) {
|
||||
return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) {
|
||||
if (function_exists('aksrt_verify_captcha')) {
|
||||
$captcha_result = aksrt_verify_captcha($post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$method = self::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
|
||||
|
||||
// 容错:meta 缺失时回退到设置默认值与 user_email(兼容历史账号 / meta 被外部清理的情况)
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = AKSRT_Settings::registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,请联系管理员', 'aksrt')];
|
||||
}
|
||||
|
||||
$requested_at = (int) get_user_meta($user_id, self::EMAIL_REQUESTED_AT_META, true);
|
||||
if ($requested_at > 0 && (time() - $requested_at) < 60) {
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
|
||||
return self::begin_email_verification($user, $method, [
|
||||
'email' => $email,
|
||||
'obk' => (string) get_user_meta($user_id, self::EMAIL_OBK_META, true),
|
||||
'force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function user_requires_email_verification($user): bool {
|
||||
return AKSRT_Auth_Email_Verification::user_requires_email_verification($user);
|
||||
}
|
||||
|
||||
private static function mark_email_verified(int $user_id): void {
|
||||
AKSRT_Auth_Email_Verification::mark_email_verified($user_id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API:管理员后台 / 内部业务调用(委托 AKSRT_Auth_Email_Verification)
|
||||
// ============================================================
|
||||
|
||||
public static function user_is_email_verified(int $user_id): bool {
|
||||
return AKSRT_Auth_Email_Verification::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
public static function get_user_verification_state(int $user_id): array {
|
||||
return AKSRT_Auth_Email_Verification::get_user_verification_state($user_id);
|
||||
}
|
||||
|
||||
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
return AKSRT_Auth_Email_Verification::admin_mark_verified($user_id, $by_admin_id);
|
||||
}
|
||||
|
||||
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
return AKSRT_Auth_Email_Verification::admin_unmark_verified($user_id, $by_admin_id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Auth_Service_Part_1b {
|
||||
|
||||
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
|
||||
return AKSRT_Auth_Email_Verification::admin_send_verification($user_id, $by_admin_id);
|
||||
}
|
||||
|
||||
public static function backfill_legacy_users_as_verified(bool $force = false): int {
|
||||
return AKSRT_Auth_Email_Verification::backfill_legacy_users_as_verified($force);
|
||||
}
|
||||
|
||||
public static function run_async_backfill_batch(int $offset = 0): void {
|
||||
AKSRT_Auth_Email_Verification::run_async_backfill_batch($offset);
|
||||
}
|
||||
|
||||
private static function delete_user_account(int $user_id): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!function_exists('wp_delete_user')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
}
|
||||
if (function_exists('wp_delete_user')) {
|
||||
wp_delete_user($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
private static function oauth_email_is_verified(array $oauth_data): bool {
|
||||
$user_data = isset($oauth_data['user_data']) && is_array($oauth_data['user_data']) ? $oauth_data['user_data'] : $oauth_data;
|
||||
if (empty($user_data['email'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($user_data['email_verified'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($user_data['provider'] ?? $oauth_data['provider'] ?? ''));
|
||||
if ($provider === 'google' && !empty($user_data['email'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($provider === 'github' && isset($user_data['email_verified'])) {
|
||||
return !empty($user_data['email_verified']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function begin_email_verification(WP_User $user, string $method, array $args = []): array {
|
||||
return AKSRT_Auth_Email_Verification::begin_email_verification($user, $method, $args);
|
||||
}
|
||||
|
||||
private static function verify_email_token(int $user_id, string $token): array {
|
||||
return AKSRT_Auth_Email_Verification::verify_email_token($user_id, $token);
|
||||
}
|
||||
|
||||
private static function verify_email_code(int $user_id, string $code): array {
|
||||
return AKSRT_Auth_Email_Verification::verify_email_code($user_id, $code);
|
||||
}
|
||||
|
||||
public static function get_reset_context(array $query): array {
|
||||
if (!empty($query['key']) || !empty($query['login']) || !empty($query['legacy'])) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => __('旧的重置密码链接已失效,请重新申请重置密码。', 'aksrt'),
|
||||
'token' => '',
|
||||
'user_id'=> 0,
|
||||
];
|
||||
}
|
||||
|
||||
$token = sanitize_text_field($query['token'] ?? '');
|
||||
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
|
||||
|
||||
if ($token === '' || $user_id <= 0) {
|
||||
return [
|
||||
'mode' => 'request',
|
||||
'user' => null,
|
||||
'error' => '',
|
||||
'token' => '',
|
||||
'user_id'=> 0,
|
||||
];
|
||||
}
|
||||
|
||||
$user = self::validate_reset_token($user_id, $token);
|
||||
if (is_wp_error($user)) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => $user->get_error_message(),
|
||||
'token' => $token,
|
||||
'user_id'=> $user_id,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => 'reset',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'token' => $token,
|
||||
'user_id'=> $user_id,
|
||||
];
|
||||
}
|
||||
|
||||
public static function request_password_reset(array $post): array {
|
||||
if (!wp_verify_nonce($post['_reset_nonce'] ?? '', 'aksrt_reset_password')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('lostpassword')) {
|
||||
if (function_exists('aksrt_verify_captcha')) {
|
||||
$captcha_result = aksrt_verify_captcha($post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$raw_login = trim((string) ($post['user_login'] ?? ''));
|
||||
if ($raw_login === '') {
|
||||
return ['success' => false, 'error' => __('请输入邮箱地址或用户名', 'aksrt')];
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if (is_email($raw_login)) {
|
||||
$user = get_user_by('email', $raw_login);
|
||||
} else {
|
||||
$user = get_user_by('login', sanitize_user($raw_login, true));
|
||||
}
|
||||
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
|
||||
}
|
||||
|
||||
$token = self::create_reset_token($user->ID);
|
||||
$sent = self::send_password_reset_email($user, $token);
|
||||
|
||||
if (!$sent) {
|
||||
return ['success' => false, 'error' => __('重置邮件发送失败,请稍后再试', 'aksrt')];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
|
||||
}
|
||||
|
||||
public static function reset_password_with_token(array $post): array {
|
||||
if (!wp_verify_nonce($post['_setpw_nonce'] ?? '', 'aksrt_set_password')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$token = sanitize_text_field($post['reset_token'] ?? '');
|
||||
$user_id = isset($post['reset_uid']) ? absint($post['reset_uid']) : 0;
|
||||
$pass1 = (string) ($post['pass1'] ?? '');
|
||||
$pass2 = (string) ($post['pass2'] ?? '');
|
||||
|
||||
$user = self::validate_reset_token($user_id, $token);
|
||||
if (is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => $user->get_error_message(), 'mode' => 'invalid'];
|
||||
}
|
||||
|
||||
$password_check = self::validate_password($pass1, $pass2);
|
||||
if ($password_check !== true) {
|
||||
return ['success' => false, 'error' => $password_check, 'mode' => 'reset'];
|
||||
}
|
||||
|
||||
wp_set_password($pass1, $user_id);
|
||||
self::clear_reset_token($user_id);
|
||||
|
||||
return ['success' => true, 'message' => __('密码已设置成功!', 'aksrt')];
|
||||
}
|
||||
|
||||
private static function create_reset_token(int $user_id): string {
|
||||
$token = wp_generate_password(48, false, false);
|
||||
update_user_meta($user_id, self::RESET_TOKEN_HASH_META, hash('sha256', $token));
|
||||
update_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, time() + self::RESET_TOKEN_TTL);
|
||||
update_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT, time());
|
||||
return $token;
|
||||
}
|
||||
|
||||
private static function validate_reset_token(int $user_id, string $token) {
|
||||
if ($user_id <= 0 || $token === '') {
|
||||
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, self::RESET_TOKEN_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, true);
|
||||
$token_hash = hash('sha256', $token);
|
||||
|
||||
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, $token_hash)) {
|
||||
return new WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private static function clear_reset_token(int $user_id): void {
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_HASH_META);
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META);
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT);
|
||||
}
|
||||
|
||||
private static function send_password_reset_email(WP_User $user, string $token): bool {
|
||||
$reset_url = aksrt_get_reset_password_url($token, $user->ID);
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('重置密码', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n有人请求重置您在 %s 的密码。\n\n请点击以下链接设置新密码:\n\n%s\n\n如果这不是您本人的操作,请忽略此邮件,您的密码不会被更改。", 'aksrt'),
|
||||
$user->user_login, $site_name, $reset_url
|
||||
);
|
||||
|
||||
return AKSRT_Settings::send_event_email(
|
||||
'password_reset', $user->user_email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $reset_url]
|
||||
);
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
* 手机号认证(注册 / 登录 / 绑定)
|
||||
* 对齐 zibll 的 signup_captcha / signin_captcha / bind_phone_captcha
|
||||
* ================================================================= */
|
||||
|
||||
/**
|
||||
* 手机号注册
|
||||
*
|
||||
* @param array $post POST 数据 (phone, code, password, invite_code)
|
||||
* @return array{success: bool, user_id?: int, error?: string, redirect?: string}
|
||||
*/
|
||||
public static function register_by_phone(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['code'] ?? ''));
|
||||
$pass = (string) ($post['password'] ?? '');
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
if (empty($code)) {
|
||||
return ['success' => false, 'error' => __('请输入验证码', 'aksrt')];
|
||||
}
|
||||
if (strlen($pass) < 6) {
|
||||
return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')];
|
||||
}
|
||||
|
||||
// 验证 SMS 验证码
|
||||
if (class_exists('AKSRT_SMS')) {
|
||||
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
|
||||
if (empty($verify_result['success'])) {
|
||||
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
|
||||
}
|
||||
|
||||
// 检查手机号是否已注册
|
||||
$existing_user = self::get_user_by_phone($phone);
|
||||
if ($existing_user) {
|
||||
return ['success' => false, 'error' => __('该手机号已注册', 'aksrt')];
|
||||
}
|
||||
|
||||
// 生成用户名(phone_ + 后4位)
|
||||
$username = 'phone_' . substr($phone, -4);
|
||||
$suffix = 1;
|
||||
$base = $username;
|
||||
while (username_exists($username)) {
|
||||
$username = $base . '_' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
$user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
|
||||
if (is_wp_error($user_id)) {
|
||||
return ['success' => false, 'error' => $user_id->get_error_message()];
|
||||
}
|
||||
|
||||
// 绑定手机号
|
||||
update_user_meta($user_id, 'aksrt_phone', $phone);
|
||||
update_user_meta($user_id, 'aksrt_phone_verified', '1');
|
||||
|
||||
// 邮箱标记为已验证(手机注册等同于已验证)
|
||||
update_user_meta($user_id, 'aksrt_email_verified', '1');
|
||||
|
||||
// 处理邀请码
|
||||
$invite_code = sanitize_key((string) ($post['invite_code'] ?? ''));
|
||||
if ($invite_code !== '' && class_exists('AKSRT_Invite')) {
|
||||
AKSRT_Invite::get_instance()->handle_registration_invite($user_id, $invite_code);
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
wp_set_current_user($user_id);
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
: home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'redirect' => $redirect,
|
||||
'message' => __('注册成功', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号登录(密码登录)
|
||||
*
|
||||
* @param array $post (phone, password)
|
||||
* @return array
|
||||
*/
|
||||
public static function login_by_phone_password(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$pass = (string) ($post['password'] ?? '');
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
$user = self::get_user_by_phone($phone);
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!wp_check_password($pass, $user->data->user_pass, $user->ID)) {
|
||||
return ['success' => false, 'error' => __('密码不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
: home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user->ID,
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号免密登录(SMS 验证码登录)
|
||||
*
|
||||
* @param array $post (phone, code)
|
||||
* @return array
|
||||
*/
|
||||
public static function login_by_phone_code(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['code'] ?? ''));
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
// 验证 SMS 验证码
|
||||
if (class_exists('AKSRT_SMS')) {
|
||||
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
|
||||
if (empty($verify_result['success'])) {
|
||||
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
|
||||
}
|
||||
|
||||
$user = self::get_user_by_phone($phone);
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
|
||||
}
|
||||
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
: home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user->ID,
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号到当前用户
|
||||
*
|
||||
* @param array $post (phone, code)
|
||||
* @return array
|
||||
*/
|
||||
public static function bind_phone(array $post): array {
|
||||
if (!is_user_logged_in()) {
|
||||
return ['success' => false, 'error' => __('请先登录', 'aksrt')];
|
||||
}
|
||||
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['code'] ?? ''));
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
// 验证 SMS 验证码
|
||||
if (class_exists('AKSRT_SMS')) {
|
||||
$verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code);
|
||||
if (empty($verify_result['success'])) {
|
||||
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
|
||||
}
|
||||
|
||||
// 检查手机号是否已被其他用户绑定
|
||||
$existing = self::get_user_by_phone($phone);
|
||||
if ($existing && (int) $existing->ID !== get_current_user_id()) {
|
||||
return ['success' => false, 'error' => __('该手机号已被其他账号绑定', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
update_user_meta($user_id, 'aksrt_phone', $phone);
|
||||
update_user_meta($user_id, 'aksrt_phone_verified', '1');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __('手机号绑定成功', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号查找用户
|
||||
*/
|
||||
private static function get_user_by_phone(string $phone): ?WP_User {
|
||||
$users = get_users([
|
||||
'meta_key' => 'aksrt_phone',
|
||||
'meta_value' => $phone,
|
||||
'number' => 1,
|
||||
'fields' => 'all',
|
||||
]);
|
||||
return !empty($users) ? $users[0] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Auth Email Verification
|
||||
*
|
||||
* 注册邮箱验证子系统:从 AKSRT_Auth_Service 拆出。
|
||||
* 负责验证状态读写、管理员操作、历史用户回填、验证流程(链接/验证码)与相关通知邮件。
|
||||
*
|
||||
* 所有方法均为 public static。常量与若干门面级 helper 仍由 AKSRT_Auth_Service 持有,
|
||||
* 本类通过 AKSRT_Auth_Service:: 引用以保持单一数据源。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Auth_Email_Verification {
|
||||
|
||||
public static function user_requires_email_verification($user): bool {
|
||||
if (!$user instanceof WP_User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AKSRT_Settings::registration_requires_email_verification()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !self::is_email_verified((int) $user->ID);
|
||||
}
|
||||
|
||||
public static function is_email_verified(int $user_id): bool {
|
||||
return (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
}
|
||||
|
||||
public static function mark_email_verified(int $user_id): void {
|
||||
$was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
|
||||
self::clear_email_verification($user_id);
|
||||
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
|
||||
// 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。
|
||||
if (!$was_verified) {
|
||||
do_action('aksrt_email_verified', $user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public static function clear_email_verification(int $user_id): void {
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_METHOD_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META);
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META);
|
||||
self::reset_email_code_attempt_trackers($user_id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API:管理员后台 / 内部业务调用
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。
|
||||
*/
|
||||
public static function user_is_email_verified(int $user_id): bool {
|
||||
return self::is_email_verified($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取一个用户当前的验证状态摘要,用于后台资料页展示。
|
||||
*
|
||||
* @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array}
|
||||
*/
|
||||
public static function get_user_verification_state(int $user_id): array {
|
||||
return [
|
||||
'verified' => self::is_email_verified($user_id),
|
||||
'method' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_METHOD_META, true),
|
||||
'target_email' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true),
|
||||
'expires_at' => (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true),
|
||||
'requested_at' => (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, true),
|
||||
'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。
|
||||
*/
|
||||
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
self::mark_email_verified($user_id);
|
||||
self::record_admin_action($user_id, 'mark_verified', $by_admin_id);
|
||||
AKSRT_Auth_Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。
|
||||
*/
|
||||
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
|
||||
self::clear_email_verification($user_id);
|
||||
self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
|
||||
AKSRT_Auth_Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')];
|
||||
}
|
||||
/**
|
||||
* 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。
|
||||
*/
|
||||
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('用户不存在', 'aksrt')];
|
||||
}
|
||||
|
||||
$method = AKSRT_Auth_Service::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = AKSRT_Settings::registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
|
||||
$result = self::begin_email_verification($user, $method, [
|
||||
'email' => $email,
|
||||
'obk' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META, true),
|
||||
'bypass_throttle' => true,
|
||||
'force' => true,
|
||||
]);
|
||||
|
||||
// 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
|
||||
if (!empty($result['success'])) {
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
|
||||
self::record_admin_action($user_id, 'send_verification', $by_admin_id);
|
||||
AKSRT_Auth_Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。
|
||||
*
|
||||
* 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。
|
||||
*
|
||||
* 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。
|
||||
*
|
||||
* @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用
|
||||
*/
|
||||
public static function backfill_legacy_users_as_verified(bool $force = false): int {
|
||||
if (!$force && get_option('aksrt_email_verification_backfill_done')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
if (function_exists('count_users')) {
|
||||
$counts = count_users();
|
||||
$total = (int) ($counts['total_users'] ?? 0);
|
||||
}
|
||||
|
||||
// 大表保护:超过 5w 用户改为后台 cron 分批
|
||||
if ($total > 50000) {
|
||||
if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) {
|
||||
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]);
|
||||
}
|
||||
update_option('aksrt_email_verification_backfill_log', [
|
||||
'mode' => 'async',
|
||||
'total' => $total,
|
||||
'scheduled' => time(),
|
||||
'by' => get_current_user_id(),
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 内联分批,每批 500
|
||||
$updated = 0;
|
||||
$paged = 1;
|
||||
do {
|
||||
$users = get_users([
|
||||
'number' => 500,
|
||||
'paged' => $paged,
|
||||
'fields' => ['ID'],
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => AKSRT_Auth_Service::EMAIL_VERIFIED_META,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (empty($users)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
update_user_meta((int) $u->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
$paged++;
|
||||
// 让 MySQL 喘口气
|
||||
if (function_exists('usleep')) {
|
||||
usleep(50000);
|
||||
}
|
||||
} while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆
|
||||
|
||||
update_option('aksrt_email_verification_backfill_done', time());
|
||||
update_option('aksrt_email_verification_backfill_log', [
|
||||
'mode' => 'inline',
|
||||
'count' => $updated,
|
||||
'ran_at' => time(),
|
||||
'by' => get_current_user_id(),
|
||||
]);
|
||||
AKSRT_Auth_Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]);
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。
|
||||
*/
|
||||
public static function run_async_backfill_batch(int $offset = 0): void {
|
||||
$users = get_users([
|
||||
'number' => 500,
|
||||
'offset' => max(0, $offset),
|
||||
'fields' => ['ID'],
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => AKSRT_Auth_Service::EMAIL_VERIFIED_META,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (empty($users)) {
|
||||
update_option('aksrt_email_verification_backfill_done', time());
|
||||
$log = (array) get_option('aksrt_email_verification_backfill_log', []);
|
||||
$log['finished_at'] = time();
|
||||
update_option('aksrt_email_verification_backfill_log', $log);
|
||||
AKSRT_Auth_Service::log_event('admin.backfill.finished', []);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
update_user_meta((int) $u->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
|
||||
}
|
||||
|
||||
// 调度下一批
|
||||
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录管理员操作日志(每个用户最多 10 条 ring buffer)。
|
||||
*/
|
||||
public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void {
|
||||
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
|
||||
if (!is_array($log)) {
|
||||
$log = [];
|
||||
}
|
||||
$log[] = [
|
||||
't' => time(),
|
||||
'action' => $action,
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
];
|
||||
if (count($log) > 10) {
|
||||
$log = array_slice($log, -10);
|
||||
}
|
||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
||||
}
|
||||
// ============================================================
|
||||
// 验证码失败计数 / IP 节流
|
||||
// ============================================================
|
||||
|
||||
public static function email_code_ip_throttle_key(int $user_id): string {
|
||||
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($user_id <= 0 || $ip === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip);
|
||||
}
|
||||
|
||||
public static function reset_email_code_attempt_trackers(int $user_id): void {
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META);
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
delete_transient($ip_key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fail_email_code_attempt(int $user_id): array {
|
||||
$attempts = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
|
||||
$attempts++;
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, $attempts);
|
||||
|
||||
$ip_attempts = 0;
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
$ip_attempts++;
|
||||
set_transient($ip_key, $ip_attempts, AKSRT_Auth_Service::EMAIL_CODE_IP_TTL);
|
||||
}
|
||||
|
||||
if ($attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
|
||||
AKSRT_Auth_Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
|
||||
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
|
||||
}
|
||||
|
||||
AKSRT_Auth_Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]);
|
||||
return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 验证流程:发起 / 校验 / 完成
|
||||
// ============================================================
|
||||
|
||||
public static function begin_email_verification(WP_User $user, string $method, array $args = []): array {
|
||||
$method = in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
$email = sanitize_email((string) ($args['email'] ?? $user->user_email));
|
||||
$obk = sanitize_text_field((string) ($args['obk'] ?? ''));
|
||||
$bypass_throttle = !empty($args['bypass_throttle']);
|
||||
if ($email === '' || !is_email($email)) {
|
||||
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')];
|
||||
}
|
||||
|
||||
// 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true)
|
||||
if (!$bypass_throttle) {
|
||||
$last_requested = (int) get_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, true);
|
||||
if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) {
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
$expires_at = time() + (AKSRT_Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_METHOD_META, $method);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TARGET_META, $email);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_EXPIRES_META, $expires_at);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, time());
|
||||
if ($obk !== '') {
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_OBK_META, $obk);
|
||||
}
|
||||
|
||||
delete_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
|
||||
delete_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
|
||||
|
||||
if ($method === 'code') {
|
||||
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_CODE_HASH_META, wp_hash_password($code));
|
||||
// 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死
|
||||
self::reset_email_code_attempt_trackers((int) $user->ID);
|
||||
$sent = self::send_registration_verification_code_email($user, $email, $code);
|
||||
} else {
|
||||
$token = wp_generate_password(48, false, false);
|
||||
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token));
|
||||
$sent = self::send_registration_verification_link_email($user, $email, $token);
|
||||
}
|
||||
|
||||
if (!$sent) {
|
||||
self::clear_email_verification((int) $user->ID);
|
||||
AKSRT_Auth_Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
|
||||
return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
|
||||
}
|
||||
|
||||
AKSRT_Auth_Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]);
|
||||
|
||||
$message = $method === 'code'
|
||||
? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt')
|
||||
: __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
public static function verify_email_token(int $user_id, string $token): array {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true);
|
||||
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) {
|
||||
return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')];
|
||||
}
|
||||
|
||||
return self::complete_email_verification($user);
|
||||
}
|
||||
|
||||
public static function verify_email_code(int $user_id, string $code): array {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
|
||||
}
|
||||
|
||||
// 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
if ($ip_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
AKSRT_Auth_Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]);
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
// 用户级失败计数:达到上限后该验证码立即作废
|
||||
$user_attempts = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
|
||||
if ($user_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
|
||||
AKSRT_Auth_Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
|
||||
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true);
|
||||
if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) {
|
||||
return self::fail_email_code_attempt($user_id);
|
||||
}
|
||||
|
||||
self::reset_email_code_attempt_trackers($user_id);
|
||||
return self::complete_email_verification($user);
|
||||
}
|
||||
|
||||
public static function complete_email_verification(WP_User $user): array {
|
||||
$user_id = (int) $user->ID;
|
||||
$obk = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_OBK_META, true);
|
||||
$oauth = $obk !== '' ? AKSRT_Auth_Service::get_oauth_bind_context($obk) : null;
|
||||
$email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
|
||||
$was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
|
||||
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
|
||||
self::clear_email_verification($user_id);
|
||||
AKSRT_Auth_Service::log_event('verify.success', ['uid' => $user_id]);
|
||||
// 状态跳变时触发 hook,邀请系统等模块据此发奖
|
||||
if (!$was_verified) {
|
||||
do_action('aksrt_email_verified', $user_id);
|
||||
}
|
||||
if ($email !== '' && is_email($email)) {
|
||||
self::send_registration_verified_notice_email($user, $email);
|
||||
}
|
||||
|
||||
if ($oauth && is_array($oauth)) {
|
||||
AKSRT_Auth_Service::finalize_oauth_registration($user_id, $oauth, $obk);
|
||||
} elseif (AKSRT_Settings::registration_auto_login_after_verify()) {
|
||||
wp_set_current_user($user_id);
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
}
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
|
||||
if (!AKSRT_Settings::registration_auto_login_after_verify()) {
|
||||
$redirect = aksrt_get_login_url();
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'),
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 通知邮件
|
||||
// ============================================================
|
||||
|
||||
public static function send_registration_verification_link_email(WP_User $user, string $email, string $token): bool {
|
||||
$url = AKSRT_Auth_Service::get_email_verification_url((int) $user->ID, ['token' => $token]);
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
|
||||
$user->user_login, $site_name, $url
|
||||
);
|
||||
|
||||
return AKSRT_Settings::send_event_email(
|
||||
'reg_verify_link', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url]
|
||||
);
|
||||
}
|
||||
|
||||
public static function send_registration_verification_code_email(WP_User $user, string $email, string $code): bool {
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
|
||||
$user->user_login, $site_name, $code
|
||||
);
|
||||
|
||||
return AKSRT_Settings::send_event_email(
|
||||
'reg_verify_code', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code]
|
||||
);
|
||||
}
|
||||
|
||||
public static function send_registration_verified_notice_email(WP_User $user, string $email): bool {
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'),
|
||||
$user->user_login, $site_name
|
||||
);
|
||||
|
||||
return AKSRT_Settings::send_event_email(
|
||||
'reg_success', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Admin
|
||||
*
|
||||
* Admin SPA container + legacy REST compatibility façade.
|
||||
* New domain routes live in inc/classes/admin/* controllers, while this class
|
||||
* remains the orchestrator and keeps only shell/bootstrap and public AJAX
|
||||
* helpers that are intentionally centralized.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_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/';
|
||||
|
||||
$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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$entry = $build_dir . 'index.js';
|
||||
if (file_exists($entry)) {
|
||||
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',
|
||||
]);
|
||||
}
|
||||
|
||||
wp_enqueue_media();
|
||||
}
|
||||
|
||||
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 {
|
||||
$controllers = [
|
||||
'AKSRT_Admin_Global_Controller',
|
||||
'AKSRT_Admin_Page_Display_Controller',
|
||||
'AKSRT_Admin_Post_List_Controller',
|
||||
'AKSRT_Admin_Permissions_Controller',
|
||||
'AKSRT_Admin_User_Controller',
|
||||
'AKSRT_Admin_Payment_Controller',
|
||||
'AKSRT_Admin_Tools_Controller',
|
||||
'AKSRT_Admin_Import_Export_Controller',
|
||||
'AKSRT_Admin_Shop_Controller',
|
||||
'AKSRT_Admin_Forum_Controller',
|
||||
'AKSRT_Admin_Extension_Controller',
|
||||
'AKSRT_Admin_Comment_Controller',
|
||||
'AKSRT_Admin_Message_Controller',
|
||||
'AKSRT_Admin_AI_Controller',
|
||||
];
|
||||
|
||||
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'])) {
|
||||
$clean['mode'] = sanitize_key((string) $config['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']);
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
public function ajax_load_more_posts(): void {
|
||||
check_ajax_referer('aksrt_public_nonce', 'nonce');
|
||||
$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) : [];
|
||||
$query_vars['paged'] = $page;
|
||||
|
||||
$query = new WP_Query($query_vars);
|
||||
ob_start();
|
||||
if (function_exists('aksrt_render_post_list')) {
|
||||
aksrt_render_post_list($query, $render_config, true);
|
||||
}
|
||||
$html = 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 {
|
||||
check_ajax_referer('aksrt_public_nonce', 'nonce');
|
||||
$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')]);
|
||||
}
|
||||
|
||||
$settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings() : [];
|
||||
$per_page = max(1, (int) get_option('comments_per_page', 10));
|
||||
$comment_query = new WP_Comment_Query([
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'number' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => !empty($settings['order']) && strtoupper((string) $settings['order']) === 'ASC' ? 'ASC' : 'DESC',
|
||||
]);
|
||||
$comments = $comment_query->comments;
|
||||
|
||||
ob_start();
|
||||
if (!empty($comments) && function_exists('aksrt_comment_callback')) {
|
||||
set_query_var('aksrt_comment_settings', $settings);
|
||||
wp_list_comments([
|
||||
'callback' => 'aksrt_comment_callback',
|
||||
'style' => 'ul',
|
||||
'avatar_size' => max(24, (int) ($settings['avatar_size'] ?? 48)),
|
||||
'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)),
|
||||
], $comments);
|
||||
}
|
||||
$html = ob_get_clean();
|
||||
|
||||
$total = get_comments([
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'count' => true,
|
||||
]);
|
||||
$max_pages = (int) ceil(((int) $total) / $per_page);
|
||||
|
||||
wp_send_json_success([
|
||||
'html' => $html,
|
||||
'has_more' => $page < $max_pages,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,800 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT After Sale — 售后服务
|
||||
*
|
||||
* 管理退款、换货等售后工单。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_After_Sale {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const TYPE_REFUND = 'refund';
|
||||
const TYPE_REFUND_RETURN = 'refund_return';
|
||||
const TYPE_EXCHANGE = 'exchange';
|
||||
const TYPE_REPAIR = 'repair';
|
||||
const TYPE_WARRANTY = 'warranty';
|
||||
const TYPE_INSURED_PRICE = 'insured_price';
|
||||
const TYPE_REPLACEMENT = 'replacement';
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_PROCESSING = 'processing';
|
||||
const STATUS_APPROVED = 'approved';
|
||||
const STATUS_REJECTED = 'rejected';
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
const PROGRESS_WAITING_USER_SHIP = 1;
|
||||
const PROGRESS_WAITING_MERCHANT = 2;
|
||||
const PROGRESS_WAITING_USER_RECEIVE = 3;
|
||||
const PROGRESS_DONE = 4;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return AKSRT_DB_Migrator::table('after_sale');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建售后工单
|
||||
*/
|
||||
public function create(array $data): int {
|
||||
global $wpdb;
|
||||
|
||||
$order_number = sanitize_text_field((string) ($data['order_number'] ?? ''));
|
||||
$user_id = (int) ($data['user_id'] ?? 0);
|
||||
$type = sanitize_key((string) ($data['type'] ?? self::TYPE_REFUND));
|
||||
$reason = sanitize_textarea_field((string) ($data['reason'] ?? ''));
|
||||
$amount = (float) ($data['amount'] ?? 0);
|
||||
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
|
||||
|
||||
if ($order_number === '' || $user_id <= 0 || $reason === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$valid_types = [self::TYPE_REFUND, self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPAIR, self::TYPE_WARRANTY, self::TYPE_INSURED_PRICE, self::TYPE_REPLACEMENT];
|
||||
if (!in_array($type, $valid_types, true)) {
|
||||
$type = self::TYPE_REFUND;
|
||||
}
|
||||
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number,
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'reason' => $reason,
|
||||
'status' => self::STATUS_PENDING,
|
||||
'amount' => $amount,
|
||||
'note' => $note,
|
||||
'created_at' => current_time('mysql'),
|
||||
]);
|
||||
|
||||
return $result ? (int) $wpdb->insert_id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取售后工单列表
|
||||
*/
|
||||
public function get_list(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
$include_order_context = !empty($args['include_order_context']);
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
$stage = sanitize_key((string) ($args['stage'] ?? ''));
|
||||
$order_number = sanitize_text_field((string) ($args['order_number'] ?? ''));
|
||||
$type = sanitize_key((string) ($args['type'] ?? ''));
|
||||
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
|
||||
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($stage !== '' && $status === '') {
|
||||
$stage_statuses = $this->map_stage_to_statuses($stage);
|
||||
if (!empty($stage_statuses)) {
|
||||
$where[] = 'status IN (' . implode(', ', array_fill(0, count($stage_statuses), '%s')) . ')';
|
||||
$params = array_merge($params, $stage_statuses);
|
||||
}
|
||||
}
|
||||
if ($order_number !== '') {
|
||||
$where[] = 'order_number = %s';
|
||||
$params[] = $order_number;
|
||||
}
|
||||
if ($type !== '') {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $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) : '';
|
||||
|
||||
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
$items = array_map([$this, 'decorate_record'], (array) $items);
|
||||
|
||||
if ($include_order_context && !empty($items) && class_exists('AKSRT_Order_Repository')) {
|
||||
$contexts = AKSRT_Order_Repository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number'));
|
||||
foreach ($items as &$item) {
|
||||
$item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? [];
|
||||
}
|
||||
unset($item);
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items ?: [],
|
||||
'total' => $total,
|
||||
'page' => floor($offset / $limit) + 1,
|
||||
'max_pages' => ceil($total / $limit),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个售后工单
|
||||
*/
|
||||
public function get(int $id): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE id = %d",
|
||||
$id
|
||||
), ARRAY_A);
|
||||
return $row ? $this->decorate_record($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个售后单并附带订单上下文。
|
||||
*/
|
||||
public function get_with_order_context(int $id): ?array {
|
||||
$record = $this->get($id);
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$record['order_context'] = class_exists('AKSRT_Order_Repository')
|
||||
? AKSRT_Order_Repository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? ''))
|
||||
: [];
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public static function get_type_options(): array {
|
||||
return [
|
||||
['value' => self::TYPE_REFUND, 'label' => __('退款', 'aksrt')],
|
||||
['value' => self::TYPE_EXCHANGE, 'label' => __('换货', 'aksrt')],
|
||||
['value' => self::TYPE_REPAIR, 'label' => __('维修', 'aksrt')],
|
||||
];
|
||||
}
|
||||
|
||||
public static function get_status_options(): array {
|
||||
return [
|
||||
['value' => self::STATUS_PENDING, 'label' => __('待处理', 'aksrt')],
|
||||
['value' => self::STATUS_PROCESSING, 'label' => __('处理中', 'aksrt')],
|
||||
['value' => self::STATUS_APPROVED, 'label' => __('已通过', 'aksrt')],
|
||||
['value' => self::STATUS_REJECTED, 'label' => __('已拒绝', 'aksrt')],
|
||||
['value' => self::STATUS_COMPLETED, 'label' => __('已完成', 'aksrt')],
|
||||
['value' => self::STATUS_CANCELLED, 'label' => __('已取消', 'aksrt')],
|
||||
];
|
||||
}
|
||||
|
||||
public static function get_stage_options(): array {
|
||||
return [
|
||||
['value' => 'review', 'label' => __('审核处理中', 'aksrt')],
|
||||
['value' => 'approved', 'label' => __('已同意,待完成', 'aksrt')],
|
||||
['value' => 'completed', 'label' => __('售后完成', 'aksrt')],
|
||||
['value' => 'rejected', 'label' => __('售后拒绝', 'aksrt')],
|
||||
['value' => 'cancelled', 'label' => __('已取消', 'aksrt')],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* R68: Get after-sale reason options from shop settings (§6.8).
|
||||
*
|
||||
* @return array{refund: array, return: array}
|
||||
*/
|
||||
public static function get_reason_options(): array {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return ['refund' => [], 'return' => []];
|
||||
}
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
$refund_text = (string) ($shop['timing']['after_sale_reason_refund'] ?? '');
|
||||
$return_text = (string) ($shop['timing']['after_sale_reason_return'] ?? '');
|
||||
|
||||
return [
|
||||
'refund' => self::parse_reason_lines($refund_text),
|
||||
'return' => self::parse_reason_lines($return_text),
|
||||
];
|
||||
}
|
||||
|
||||
private static function parse_reason_lines(string $text): array {
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
$lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text)));
|
||||
$result = [];
|
||||
foreach (array_values($lines) as $i => $line) {
|
||||
$result[] = [
|
||||
'id' => $i + 1,
|
||||
'label' => $line,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $order_numbers
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function get_summary_map(array $order_numbers): array {
|
||||
global $wpdb;
|
||||
|
||||
$order_numbers = array_values(array_unique(array_filter(array_map(
|
||||
static function ($order_number): string {
|
||||
return sanitize_text_field((string) $order_number);
|
||||
},
|
||||
$order_numbers
|
||||
))));
|
||||
|
||||
if (empty($order_numbers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
foreach ($order_numbers as $order_number) {
|
||||
$map[$order_number] = [
|
||||
'count' => 0,
|
||||
'latest' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$placeholders = implode(', ', array_fill(0, count($order_numbers), '%s'));
|
||||
$sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC";
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A);
|
||||
|
||||
foreach ((array) $rows as $row) {
|
||||
$record = $this->decorate_record($row);
|
||||
$order_number = (string) ($record['order_number'] ?? '');
|
||||
|
||||
if (!isset($map[$order_number])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$order_number]['count']++;
|
||||
if (empty($map[$order_number]['latest'])) {
|
||||
$map[$order_number]['latest'] = $this->build_summary_item($record);
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新售后工单状态
|
||||
*/
|
||||
public function update_status(int $id, string $new_status, int $operator_id = 0, string $operator_note = ''): bool {
|
||||
global $wpdb;
|
||||
|
||||
$record = $this->get($id);
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_COMPLETED, self::STATUS_CANCELLED];
|
||||
if (!in_array($new_status, $valid, true)) return false;
|
||||
if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$update = [
|
||||
'status' => $new_status,
|
||||
'updated_at' => current_time('mysql'),
|
||||
];
|
||||
|
||||
if ($operator_id > 0) {
|
||||
$update['operator_id'] = $operator_id;
|
||||
}
|
||||
if ($operator_note !== '') {
|
||||
$update['operator_note'] = sanitize_textarea_field($operator_note);
|
||||
}
|
||||
if (in_array($new_status, [self::STATUS_COMPLETED, self::STATUS_REJECTED], true)) {
|
||||
$update['resolved_at'] = current_time('mysql');
|
||||
}
|
||||
|
||||
$updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]);
|
||||
if ($updated) {
|
||||
$note = $operator_note !== '' ? $operator_note : sprintf(__('售后状态更新为:%s', 'aksrt'), $this->get_status_label($new_status));
|
||||
$this->sync_order_status($record, $new_status, $note);
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
|
||||
$counts = $wpdb->get_results(
|
||||
"SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status",
|
||||
ARRAY_A
|
||||
);
|
||||
$counts = array_column($counts, 'cnt', 'status');
|
||||
|
||||
$stats = [
|
||||
'total' => 0,
|
||||
'pending' => 0,
|
||||
'processing' => 0,
|
||||
'approved' => 0,
|
||||
'rejected' => 0,
|
||||
'completed' => 0,
|
||||
'cancelled' => 0,
|
||||
];
|
||||
|
||||
foreach ($counts as $status => $cnt) {
|
||||
if (isset($stats[$status])) {
|
||||
$stats[$status] = (int) $cnt;
|
||||
}
|
||||
$stats['total'] += (int) $cnt;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
private function decorate_record(array $record): array {
|
||||
$record['id'] = (int) ($record['id'] ?? 0);
|
||||
$record['user_id'] = (int) ($record['user_id'] ?? 0);
|
||||
$record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0;
|
||||
$record['amount'] = (float) ($record['amount'] ?? 0);
|
||||
$record['status_label'] = $this->get_status_label((string) ($record['status'] ?? ''));
|
||||
$record['type_label'] = $this->get_type_label((string) ($record['type'] ?? ''));
|
||||
$record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0));
|
||||
$record['applicant'] = $this->get_user_summary((int) ($record['user_id'] ?? 0));
|
||||
$record['stage'] = $this->build_stage_payload($record);
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function build_stage_payload(array $record): array {
|
||||
$status = (string) ($record['status'] ?? '');
|
||||
|
||||
if (in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) {
|
||||
return [
|
||||
'key' => 'review',
|
||||
'label' => __('审核处理中', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === self::STATUS_APPROVED) {
|
||||
return [
|
||||
'key' => 'approved',
|
||||
'label' => __('已同意,待完成', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === self::STATUS_COMPLETED) {
|
||||
return [
|
||||
'key' => 'completed',
|
||||
'label' => __('售后完成', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === self::STATUS_REJECTED) {
|
||||
return [
|
||||
'key' => 'rejected',
|
||||
'label' => __('售后拒绝', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'key' => 'cancelled',
|
||||
'label' => __('已取消', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_status_label(string $status): string {
|
||||
foreach (self::get_status_options() as $item) {
|
||||
if (($item['value'] ?? '') === $status) {
|
||||
return (string) ($item['label'] ?? $status);
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function get_type_label(string $type): string {
|
||||
foreach (self::get_type_options() as $item) {
|
||||
if (($item['value'] ?? '') === $type) {
|
||||
return (string) ($item['label'] ?? $type);
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function get_user_summary(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $user_id,
|
||||
'display_name' => (string) $user->display_name,
|
||||
'user_login' => (string) $user->user_login,
|
||||
'user_email' => (string) $user->user_email,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function map_stage_to_statuses(string $stage): array {
|
||||
$map = [
|
||||
'review' => [self::STATUS_PENDING, self::STATUS_PROCESSING],
|
||||
'approved' => [self::STATUS_APPROVED],
|
||||
'completed' => [self::STATUS_COMPLETED],
|
||||
'rejected' => [self::STATUS_REJECTED],
|
||||
'cancelled' => [self::STATUS_CANCELLED],
|
||||
];
|
||||
|
||||
return $map[$stage] ?? [];
|
||||
}
|
||||
|
||||
private function can_transition_status(string $current_status, string $new_status): bool {
|
||||
$map = [
|
||||
self::STATUS_PENDING => [self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED],
|
||||
self::STATUS_PROCESSING => [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED],
|
||||
self::STATUS_APPROVED => [self::STATUS_COMPLETED, self::STATUS_CANCELLED],
|
||||
self::STATUS_REJECTED => [],
|
||||
self::STATUS_COMPLETED => [],
|
||||
self::STATUS_CANCELLED => [],
|
||||
];
|
||||
|
||||
return in_array($new_status, $map[$current_status] ?? [], true);
|
||||
}
|
||||
|
||||
private function build_summary_item(array $record): array {
|
||||
return [
|
||||
'id' => (int) ($record['id'] ?? 0),
|
||||
'type' => (string) ($record['type'] ?? ''),
|
||||
'type_label' => (string) ($record['type_label'] ?? ''),
|
||||
'status' => (string) ($record['status'] ?? ''),
|
||||
'status_label' => (string) ($record['status_label'] ?? ''),
|
||||
'stage' => is_array($record['stage'] ?? null) ? $record['stage'] : [],
|
||||
'amount' => (float) ($record['amount'] ?? 0),
|
||||
'created_at' => (string) ($record['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($record['updated_at'] ?? ''),
|
||||
'resolved_at' => (string) ($record['resolved_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* R69: Execute actual refund — credit back user balance / points.
|
||||
*/
|
||||
private function execute_refund(?array $order, float $refund_amount): void {
|
||||
if (!$order || $refund_amount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_method = (string) ($order['payment_method'] ?? 'balance');
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
|
||||
// Balance refund: credit back via AKSRT_Credits
|
||||
if ($payment_method === 'balance' && class_exists('AKSRT_Credits')) {
|
||||
AKSRT_Credits::add(
|
||||
$user_id,
|
||||
(int) round($refund_amount),
|
||||
sprintf(__('售后退款:订单 %s', 'aksrt'), $order_number),
|
||||
['order_number' => $order_number, 'type' => 'refund'],
|
||||
sprintf(__('售后退款金额 ¥%s', 'aksrt'), number_format($refund_amount, 2))
|
||||
);
|
||||
}
|
||||
|
||||
// Deduct sales_volume for refunded items
|
||||
$meta = json_decode((string) ($order['note'] ?? ''), true);
|
||||
$items = is_array($meta) ? ($meta['items'] ?? []) : [];
|
||||
if (empty($items) && !empty($order['product_id'])) {
|
||||
$items = [['product_id' => (int) $order['product_id'], 'quantity' => 1]];
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$pid = (int) ($item['product_id'] ?? 0);
|
||||
$qty = max(1, (int) ($item['quantity'] ?? 1));
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
|
||||
update_post_meta($pid, '_aksrt_sales_volume', max(0, $sales - $qty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after refund execution.
|
||||
*
|
||||
* @param string $order_number
|
||||
* @param int $user_id
|
||||
* @param float $refund_amount
|
||||
* @param string $payment_method
|
||||
*/
|
||||
do_action('aksrt_shop_refund_executed', $order_number, $user_id, $refund_amount, $payment_method);
|
||||
}
|
||||
|
||||
private function sync_order_status(array $record, string $new_status, string $note = ''): void {
|
||||
if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Order')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = (string) ($record['type'] ?? '');
|
||||
$is_refund_type = in_array($type, [self::TYPE_REFUND, self::TYPE_REFUND_RETURN, self::TYPE_INSURED_PRICE], true);
|
||||
if (!$is_refund_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) ($record['order_number'] ?? ''));
|
||||
if ($order_number === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$repository = AKSRT_Order_Repository::get_instance();
|
||||
|
||||
if (in_array($new_status, [self::STATUS_PROCESSING, self::STATUS_APPROVED], true)) {
|
||||
$repository->update_status($order_number, AKSRT_Order::STATUS_REFUNDING, [
|
||||
'note' => $note,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($new_status === self::STATUS_COMPLETED) {
|
||||
$order = $repository->get($order_number);
|
||||
if (($order['status'] ?? '') !== AKSRT_Order::STATUS_REFUNDING) {
|
||||
$repository->update_status($order_number, AKSRT_Order::STATUS_REFUNDING, [
|
||||
'note' => $note,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->execute_refund($order, (float) ($record['amount'] ?? 0));
|
||||
|
||||
$repository->update_status($order_number, AKSRT_Order::STATUS_REFUNDED, [
|
||||
'note' => $note,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Merchant Actions (§6.8)
|
||||
* ============================================================ */
|
||||
|
||||
public function merchant_approve(int $ticket_id, int $operator_id, string $note = ''): bool {
|
||||
global $wpdb;
|
||||
$record = $this->get_by_id($ticket_id);
|
||||
if (!$record || $record['status'] !== self::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = (string) $record['type'];
|
||||
$needs_return = in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true);
|
||||
$new_status = $needs_return ? self::STATUS_PROCESSING : self::STATUS_APPROVED;
|
||||
$progress = $needs_return ? self::PROGRESS_WAITING_USER_SHIP : self::PROGRESS_WAITING_MERCHANT;
|
||||
|
||||
$wpdb->update($this->table(), [
|
||||
'status' => $new_status,
|
||||
'operator_id' => $operator_id,
|
||||
'operator_note' => sanitize_textarea_field($note),
|
||||
'updated_at' => current_time('mysql'),
|
||||
], ['id' => $ticket_id]);
|
||||
|
||||
update_post_meta($ticket_id, '_aksrt_as_progress', $progress);
|
||||
$this->sync_order_status($record, $new_status, $note ?: __('商家已同意售后', 'aksrt'));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function merchant_reject(int $ticket_id, int $operator_id, string $note = ''): bool {
|
||||
global $wpdb;
|
||||
$record = $this->get_by_id($ticket_id);
|
||||
if (!$record || $record['status'] !== self::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wpdb->update($this->table(), [
|
||||
'status' => self::STATUS_REJECTED,
|
||||
'operator_id' => $operator_id,
|
||||
'operator_note' => sanitize_textarea_field($note),
|
||||
'updated_at' => current_time('mysql'),
|
||||
], ['id' => $ticket_id]);
|
||||
|
||||
update_post_meta($ticket_id, '_aksrt_as_progress', 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function user_submit_return_shipping(int $ticket_id, int $user_id, string $carrier, string $tracking_no): bool {
|
||||
global $wpdb;
|
||||
$record = $this->get_by_id($ticket_id);
|
||||
if (!$record || (int) ($record['user_id'] ?? 0) !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
if ($record['status'] !== self::STATUS_PROCESSING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$return_data = wp_json_encode([
|
||||
'carrier' => sanitize_text_field($carrier),
|
||||
'tracking_no' => sanitize_text_field($tracking_no),
|
||||
'submitted_at' => current_time('mysql'),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$wpdb->update($this->table(), [
|
||||
'note' => $return_data,
|
||||
'updated_at' => current_time('mysql'),
|
||||
], ['id' => $ticket_id]);
|
||||
|
||||
update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_WAITING_MERCHANT);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function merchant_confirm_return(int $ticket_id, int $operator_id, string $note = ''): bool {
|
||||
global $wpdb;
|
||||
$record = $this->get_by_id($ticket_id);
|
||||
if (!$record || $record['status'] !== self::STATUS_PROCESSING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = (string) $record['type'];
|
||||
|
||||
if (in_array($type, [self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true)) {
|
||||
update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_WAITING_USER_RECEIVE);
|
||||
$wpdb->update($this->table(), [
|
||||
'operator_note' => sanitize_textarea_field($note),
|
||||
'updated_at' => current_time('mysql'),
|
||||
], ['id' => $ticket_id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
$wpdb->update($this->table(), [
|
||||
'status' => self::STATUS_COMPLETED,
|
||||
'operator_id' => $operator_id,
|
||||
'operator_note' => sanitize_textarea_field($note),
|
||||
'updated_at' => current_time('mysql'),
|
||||
'resolved_at' => current_time('mysql'),
|
||||
], ['id' => $ticket_id]);
|
||||
|
||||
update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_DONE);
|
||||
$this->sync_order_status($record, self::STATUS_COMPLETED, $note ?: __('售后已完成', 'aksrt'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Shipping status window + type constraints (§6.8)
|
||||
* ============================================================ */
|
||||
|
||||
public function can_apply_after_sale(array $order, string $type): bool {
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
if (!in_array($status, [
|
||||
AKSRT_Order::STATUS_PAID,
|
||||
AKSRT_Order::STATUS_DELIVERED,
|
||||
AKSRT_Order::STATUS_COMPLETED,
|
||||
AKSRT_Order::STATUS_PENDING_REVIEW,
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$shipping_status = (int) ($order['shipping_status'] ?? 0);
|
||||
|
||||
if ($type === self::TYPE_REFUND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true)) {
|
||||
return $shipping_status >= AKSRT_Domain_Model::SHIPPING_STATUS_SHIPPED;
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_WARRANTY) {
|
||||
return $shipping_status >= AKSRT_Domain_Model::SHIPPING_STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_type_timeouts(): array {
|
||||
$shop = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : [];
|
||||
$timing = $shop['timing'] ?? [];
|
||||
return [
|
||||
self::TYPE_REFUND => (int) ($timing['refund_max_day'] ?? 7),
|
||||
self::TYPE_REFUND_RETURN => (int) ($timing['refund_return_max_day'] ?? 7),
|
||||
self::TYPE_EXCHANGE => (int) ($timing['exchange_max_day'] ?? 7),
|
||||
self::TYPE_REPLACEMENT => (int) ($timing['replacement_max_day'] ?? 7),
|
||||
self::TYPE_WARRANTY => (int) ($timing['warranty_max_day'] ?? 30),
|
||||
self::TYPE_INSURED_PRICE => (int) ($timing['insured_price_max_day'] ?? 15),
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Auto-cancel Cron (§6.8)
|
||||
* ============================================================ */
|
||||
|
||||
public function run_auto_cancel_cron(): void {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
|
||||
$timeouts = $this->get_type_timeouts();
|
||||
|
||||
foreach ($timeouts as $type => $max_days) {
|
||||
if ($max_days <= 0) continue;
|
||||
|
||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_days} days"));
|
||||
|
||||
$stale = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, order_number, type, status FROM {$table} WHERE type = %s AND status IN (%s, %s) AND created_at < %s LIMIT 200",
|
||||
$type, self::STATUS_PENDING, self::STATUS_PROCESSING, $cutoff
|
||||
), ARRAY_A);
|
||||
|
||||
foreach ($stale as $record) {
|
||||
$wpdb->update($table, [
|
||||
'status' => self::STATUS_CANCELLED,
|
||||
'updated_at' => current_time('mysql'),
|
||||
'operator_note' => sprintf(__('系统自动取消(超过 %d 天未处理)', 'aksrt'), $max_days),
|
||||
], ['id' => (int) $record['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function get_by_id(int $id): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE id = %d",
|
||||
$id
|
||||
), ARRAY_A);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public static function get_valid_types(): array {
|
||||
return [
|
||||
self::TYPE_REFUND => __('仅退款', 'aksrt'),
|
||||
self::TYPE_REFUND_RETURN => __('退货退款', 'aksrt'),
|
||||
self::TYPE_EXCHANGE => __('换货', 'aksrt'),
|
||||
self::TYPE_REPLACEMENT => __('补发', 'aksrt'),
|
||||
self::TYPE_WARRANTY => __('保修', 'aksrt'),
|
||||
self::TYPE_INSURED_PRICE => __('保价', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function get_progress_labels(): array {
|
||||
return [
|
||||
self::PROGRESS_WAITING_USER_SHIP => __('等待用户发货', 'aksrt'),
|
||||
self::PROGRESS_WAITING_MERCHANT => __('等待商家处理', 'aksrt'),
|
||||
self::PROGRESS_WAITING_USER_RECEIVE => __('等待用户收货', 'aksrt'),
|
||||
self::PROGRESS_DONE => __('已完成', 'aksrt'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT AI Prompt — AI Prompt 模板管理
|
||||
*
|
||||
* 管理各种 AI 功能的 prompt 模板。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_AI_Prompt {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 SEO 建议 prompt
|
||||
*
|
||||
* 优先读取后台 ai_seo_*_prompt 设置,为空则使用内置默认提示词。
|
||||
*/
|
||||
public function build_seo_prompt(string $title, string $content): string {
|
||||
// 读取后台自定义提示词
|
||||
$custom_title = '';
|
||||
$custom_kw = '';
|
||||
$custom_desc = '';
|
||||
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$s = AKSRT_Settings::get_all();
|
||||
$custom_title = trim((string) ($s['ai_seo_title_prompt'] ?? ''));
|
||||
$custom_kw = trim((string) ($s['ai_seo_keywords_prompt'] ?? ''));
|
||||
$custom_desc = trim((string) ($s['ai_seo_description_prompt'] ?? ''));
|
||||
}
|
||||
|
||||
// 内置默认提示词(zibll 风格长提示词)
|
||||
$title_instruction = $custom_title ?: '请为文章生成一个 SEO 友好的标题,要求:包含核心关键词,控制在 60 个字符以内,语言自然流畅,吸引用户点击。';
|
||||
$kw_instruction = $custom_kw ?: '请为文章提取 5-8 个 SEO 关键词,要求:涵盖核心主题词和长尾词,按搜索热度排序,用英文逗号分隔。';
|
||||
$desc_instruction = $custom_desc ?: '请为文章生成一段 SEO 描述(meta description),要求:160 个字符以内,准确概括文章内容,包含核心关键词,具有吸引力能促进点击。';
|
||||
|
||||
return <<<PROMPT
|
||||
请根据以下文章内容,生成 SEO 优化建议。
|
||||
|
||||
文章标题:{$title}
|
||||
|
||||
文章内容:
|
||||
{$content}
|
||||
|
||||
请以 JSON 格式返回,包含以下字段:
|
||||
- suggested_title: {$title_instruction}
|
||||
- suggested_description: {$desc_instruction}
|
||||
- suggested_keywords: {$kw_instruction}(以 JSON 数组形式返回)
|
||||
|
||||
只返回 JSON,不要其他内容。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建内容摘要 prompt
|
||||
*/
|
||||
public function build_summary_prompt(string $content, int $max_length = 200): string {
|
||||
$content = mb_substr($content, 0, 3000);
|
||||
return <<<PROMPT
|
||||
请为以下内容生成一段简洁的摘要,不超过 {$max_length} 个字。
|
||||
|
||||
内容:
|
||||
{$content}
|
||||
|
||||
只返回摘要文本,不要其他内容。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建标签建议 prompt
|
||||
*/
|
||||
public function build_tags_prompt(string $title, string $content): string {
|
||||
$content = mb_substr($content, 0, 2000);
|
||||
return <<<PROMPT
|
||||
请根据以下文章内容,建议合适的标签。
|
||||
|
||||
文章标题:{$title}
|
||||
|
||||
文章内容:
|
||||
{$content}
|
||||
|
||||
请以 JSON 数组格式返回标签列表(5-10个),例如:["标签1", "标签2"]
|
||||
只返回 JSON 数组,不要其他内容。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建摘要建议 prompt
|
||||
*/
|
||||
public function build_excerpt_prompt(string $title, string $content): string {
|
||||
$content = mb_substr($content, 0, 2000);
|
||||
return <<<PROMPT
|
||||
请为以下文章生成一段吸引人的摘要(excerpt),用于列表页展示。
|
||||
|
||||
文章标题:{$title}
|
||||
|
||||
文章内容:
|
||||
{$content}
|
||||
|
||||
要求:
|
||||
- 50-100字
|
||||
- 突出文章核心价值
|
||||
- 吸引读者点击
|
||||
|
||||
只返回摘要文本,不要其他内容。
|
||||
PROMPT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT AI SEO — AI SEO 辅助
|
||||
*
|
||||
* 提供 SEO 标题、描述、关键词建议。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_AI_SEO {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 SEO 建议
|
||||
*
|
||||
* @param string $title 文章标题
|
||||
* @param string $content 文章内容(截取前2000字)
|
||||
* @return array{success: bool, data?: array, message?: string}
|
||||
*/
|
||||
public function suggest(string $title, string $content = ''): array {
|
||||
// seo_ai_enabled 主开关
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$seo = AKSRT_Settings::get_seo_settings();
|
||||
if (empty($seo['seo_ai_enabled'])) {
|
||||
return ['success' => false, 'message' => 'AI SEO 功能未启用'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_AI')) {
|
||||
return ['success' => false, 'message' => 'AI 模块未加载'];
|
||||
}
|
||||
|
||||
$ai = AKSRT_AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return ['success' => false, 'message' => 'AI 服务未启用'];
|
||||
}
|
||||
|
||||
// 截取内容避免 token 超限
|
||||
$content = mb_substr($content, 0, 2000);
|
||||
|
||||
$prompt = AKSRT_AI_Prompt::get_instance()->build_seo_prompt($title, $content);
|
||||
|
||||
$result = $ai->generate($prompt, [
|
||||
'max_tokens' => 500,
|
||||
'temperature' => 0.5,
|
||||
]);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 解析 JSON 响应
|
||||
$parsed = $this->parse_seo_response($result['content']);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $parsed,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 SEO 响应
|
||||
*/
|
||||
private function parse_seo_response(string $content): array {
|
||||
$defaults = [
|
||||
'suggested_title' => '',
|
||||
'suggested_description' => '',
|
||||
'suggested_keywords' => [],
|
||||
];
|
||||
|
||||
// 尝试 JSON 解析
|
||||
$json = json_decode($content, true);
|
||||
if (is_array($json)) {
|
||||
return array_merge($defaults, $json);
|
||||
}
|
||||
|
||||
// 尝试从文本中提取
|
||||
$lines = explode("\n", $content);
|
||||
$result = $defaults;
|
||||
$current_key = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) continue;
|
||||
|
||||
if (stripos($line, '标题') !== false || stripos($line, 'title') !== false) {
|
||||
$current_key = 'suggested_title';
|
||||
$result[$current_key] = trim(preg_replace('/^[^::]+[::]\s*/', '', $line));
|
||||
} elseif (stripos($line, '描述') !== false || stripos($line, 'description') !== false) {
|
||||
$current_key = 'suggested_description';
|
||||
$result[$current_key] = trim(preg_replace('/^[^::]+[::]\s*/', '', $line));
|
||||
} elseif (stripos($line, '关键词') !== false || stripos($line, 'keywords') !== false) {
|
||||
$current_key = 'suggested_keywords';
|
||||
$keywords_str = trim(preg_replace('/^[^::]+[::]\s*/', '', $line));
|
||||
$result[$current_key] = array_map('trim', explode(',', $keywords_str));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT AI — AI 子系统核心
|
||||
*
|
||||
* 统一管理 AI provider 调用、速率限制和输出处理。
|
||||
* 默认关闭,需管理员显式启用。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_AI {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const PROVIDER_NONE = 'none';
|
||||
const PROVIDER_OPENAI = 'openai';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 配置
|
||||
*/
|
||||
public function get_config(): array {
|
||||
$defaults = [
|
||||
'enabled' => false,
|
||||
'provider' => self::PROVIDER_NONE,
|
||||
'api_key' => '',
|
||||
'api_base' => 'https://api.openai.com/v1',
|
||||
'model' => 'gpt-3.5-turbo',
|
||||
'max_tokens' => 1000,
|
||||
'rate_limit' => 10, // 每分钟最大调用次数
|
||||
];
|
||||
|
||||
$saved = get_option('aksrt_ai_settings', []);
|
||||
if (!is_array($saved)) $saved = [];
|
||||
|
||||
return array_merge($defaults, $saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 AI 配置
|
||||
*/
|
||||
public function update_config(array $config): bool {
|
||||
$current = $this->get_config();
|
||||
$merged = array_merge($current, $config);
|
||||
|
||||
// 如果 api_key 为掩码,保留原值
|
||||
if (isset($config['api_key']) && $config['api_key'] === '***') {
|
||||
$merged['api_key'] = $current['api_key'];
|
||||
}
|
||||
|
||||
return update_option('aksrt_ai_settings', $merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 AI 是否可用
|
||||
*/
|
||||
public function is_available(): bool {
|
||||
$config = $this->get_config();
|
||||
return !empty($config['enabled'])
|
||||
&& $config['provider'] !== self::PROVIDER_NONE
|
||||
&& !empty($config['api_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 状态
|
||||
*/
|
||||
public function get_status(): array {
|
||||
$config = $this->get_config();
|
||||
|
||||
return [
|
||||
'enabled' => !empty($config['enabled']),
|
||||
'provider' => $config['provider'],
|
||||
'configured' => !empty($config['api_key']),
|
||||
'model' => $config['model'],
|
||||
'api_key' => !empty($config['api_key']) ? '***' : '',
|
||||
'rate_limit' => $config['rate_limit'],
|
||||
'available' => $this->is_available(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 AI 生成
|
||||
*
|
||||
* @param string $prompt 提示词
|
||||
* @param array $options 选项
|
||||
* @return array{success: bool, content?: string, message?: string}
|
||||
*/
|
||||
public function generate(string $prompt, array $options = []): array {
|
||||
if (!$this->is_available()) {
|
||||
return ['success' => false, 'message' => 'AI 服务未启用或未配置'];
|
||||
}
|
||||
|
||||
// 速率限制检查
|
||||
if (!$this->check_rate_limit()) {
|
||||
return ['success' => false, 'message' => '请求过于频繁,请稍后再试'];
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
$max_tokens = (int) ($options['max_tokens'] ?? $config['max_tokens']);
|
||||
$temperature = (float) ($options['temperature'] ?? 0.7);
|
||||
|
||||
$response = wp_remote_post($config['api_base'] . '/chat/completions', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $config['api_key'],
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'model' => $config['model'],
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => '你是一个内容助手,提供建议性内容。输出仅作为参考建议,不会自动应用到线上内容。'],
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
'max_tokens' => $max_tokens,
|
||||
'temperature' => $temperature,
|
||||
]),
|
||||
'timeout' => 30,
|
||||
]);
|
||||
|
||||
// 记录调用
|
||||
$this->record_call();
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return ['success' => false, 'message' => 'API 请求失败: ' . $response->get_error_message()];
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status_code !== 200) {
|
||||
$error_msg = $body['error']['message'] ?? '未知错误';
|
||||
return ['success' => false, 'message' => 'API 错误 (' . $status_code . '): ' . $error_msg];
|
||||
}
|
||||
|
||||
$message = $body['choices'][0]['message'] ?? [];
|
||||
$content = $message['content'] ?? '';
|
||||
|
||||
// 兼容推理模型(如 mimo-v2.5-pro):content 为空时取 reasoning_content
|
||||
if (empty($content) && !empty($message['reasoning_content'])) {
|
||||
$content = $message['reasoning_content'];
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
return ['success' => false, 'message' => 'AI 返回内容为空(可能 max_tokens 不足)'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => trim($content),
|
||||
'reasoning' => $message['reasoning_content'] ?? '',
|
||||
'usage' => $body['usage'] ?? [],
|
||||
'finish_reason' => $body['choices'][0]['finish_reason'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 速率限制检查
|
||||
*/
|
||||
private function check_rate_limit(): bool {
|
||||
$config = $this->get_config();
|
||||
$limit = (int) $config['rate_limit'];
|
||||
if ($limit <= 0) return true;
|
||||
|
||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
||||
$count = (int) get_transient($key);
|
||||
|
||||
return $count < $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录调用
|
||||
*/
|
||||
private function record_call(): void {
|
||||
$key = 'aksrt_ai_rate_' . get_current_user_id();
|
||||
$count = (int) get_transient($key);
|
||||
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT API Audit Logger
|
||||
*
|
||||
* Logs sensitive REST API operations for security auditing.
|
||||
* Does NOT log passwords, secrets, or private keys.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_API_Audit {
|
||||
|
||||
private static $instance = null;
|
||||
private static $table_name = '';
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
global $wpdb;
|
||||
self::$table_name = $wpdb->prefix . 'aksrt_api_audit';
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_hooks']);
|
||||
add_action('aksrt_db_migrate', [$this, 'maybe_create_table']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if audit logging is enabled
|
||||
*/
|
||||
private function is_enabled(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) return false;
|
||||
return (bool) AKSRT_Settings::get_value('api_audit_enabled', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST pre-dispatch hook to log requests
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
if (!$this->is_enabled()) return;
|
||||
|
||||
// Log after response to capture status code
|
||||
add_filter('rest_post_dispatch', [$this, 'log_request'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a REST API request
|
||||
*/
|
||||
public function log_request($result, $server, $request) {
|
||||
$route = $request->get_route();
|
||||
$method = $request->get_method();
|
||||
|
||||
// Only log write operations (POST, PUT, DELETE, PATCH)
|
||||
if (!in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'], true)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Skip non-aksrt routes
|
||||
if (strpos($route, '/aksrt/') === false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Skip health checks and non-sensitive routes
|
||||
$skip_patterns = ['/health', '/settings/export', '/settings/import'];
|
||||
foreach ($skip_patterns as $pattern) {
|
||||
if (strpos($route, $pattern) !== false) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$ip = $this->get_client_ip();
|
||||
$status = $result instanceof \WP_REST_Response ? $result->get_status() : 200;
|
||||
|
||||
$this->insert_log([
|
||||
'user_id' => $user_id,
|
||||
'method' => $method,
|
||||
'route' => $route,
|
||||
'ip' => $ip,
|
||||
'status' => $status,
|
||||
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually log an event
|
||||
*/
|
||||
public static function log(string $action, array $context = [], int $user_id = 0): void {
|
||||
$instance = self::get_instance();
|
||||
if (!$instance->is_enabled()) return;
|
||||
|
||||
$instance->insert_log([
|
||||
'user_id' => $user_id ?: get_current_user_id(),
|
||||
'method' => 'ACTION',
|
||||
'route' => $action,
|
||||
'ip' => $instance->get_client_ip(),
|
||||
'status' => 200,
|
||||
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
|
||||
'context' => wp_json_encode($context, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a log entry
|
||||
*/
|
||||
private function insert_log(array $data): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->insert(self::$table_name, [
|
||||
'user_id' => $data['user_id'] ?? 0,
|
||||
'method' => $data['method'] ?? '',
|
||||
'route' => $data['route'] ?? '',
|
||||
'ip' => $data['ip'] ?? '',
|
||||
'status' => $data['status'] ?? 0,
|
||||
'user_agent' => $data['user_agent'] ?? '',
|
||||
'context' => $data['context'] ?? '',
|
||||
'created_at' => current_time('mysql', true),
|
||||
], ['%d', '%s', '%s', '%s', '%d', '%s', '%s', '%s']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP
|
||||
*/
|
||||
private function get_client_ip(): string {
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$ip = trim($parts[0]);
|
||||
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return sanitize_text_field($ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create audit table if not exists
|
||||
*/
|
||||
public function maybe_create_table(): void {
|
||||
global $wpdb;
|
||||
$table = self::$table_name;
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
method VARCHAR(10) NOT NULL DEFAULT '',
|
||||
route VARCHAR(255) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(45) NOT NULL DEFAULT '',
|
||||
status SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
user_agent VARCHAR(255) NOT NULL DEFAULT '',
|
||||
context TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_user_id (user_id),
|
||||
KEY idx_route (route),
|
||||
KEY idx_created_at (created_at)
|
||||
) {$charset};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query audit logs with pagination
|
||||
*/
|
||||
public function query_logs(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$defaults = [
|
||||
'per_page' => 20,
|
||||
'page' => 1,
|
||||
'user_id' => 0,
|
||||
'route' => '',
|
||||
'method' => '',
|
||||
'from' => '',
|
||||
'to' => '',
|
||||
];
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$where = ['1=1'];
|
||||
$values = [];
|
||||
|
||||
if ($args['user_id'] > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$values[] = $args['user_id'];
|
||||
}
|
||||
if (!empty($args['route'])) {
|
||||
$where[] = 'route LIKE %s';
|
||||
$values[] = '%' . $wpdb->esc_like($args['route']) . '%';
|
||||
}
|
||||
if (!empty($args['method'])) {
|
||||
$where[] = 'method = %s';
|
||||
$values[] = $args['method'];
|
||||
}
|
||||
if (!empty($args['from'])) {
|
||||
$where[] = 'created_at >= %s';
|
||||
$values[] = $args['from'];
|
||||
}
|
||||
if (!empty($args['to'])) {
|
||||
$where[] = 'created_at <= %s';
|
||||
$values[] = $args['to'];
|
||||
}
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
$offset = ($args['page'] - 1) * $args['per_page'];
|
||||
|
||||
$total = (int) $wpdb->get_var(
|
||||
$wpdb->prepare("SELECT COUNT(*) FROM " . self::$table_name . " WHERE {$where_sql}", $values)
|
||||
);
|
||||
|
||||
$values[] = $args['per_page'];
|
||||
$values[] = $offset;
|
||||
|
||||
$logs = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM " . self::$table_name . " WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$values
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'per_page'=> $args['per_page'],
|
||||
'page' => $args['page'],
|
||||
'logs' => $logs ?: [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge old logs
|
||||
*/
|
||||
public function purge_old(int $days = 90): int {
|
||||
global $wpdb;
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||
return (int) $wpdb->query(
|
||||
$wpdb->prepare("DELETE FROM " . self::$table_name . " WHERE created_at < %s", $cutoff)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Auth Router
|
||||
*
|
||||
* 统一处理主题内认证路由:
|
||||
* - /login
|
||||
* - /register
|
||||
* - /bind-account
|
||||
* - /reset-password
|
||||
* - /logout
|
||||
* - wp-login.php 前台分流
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class AKSRT_Auth_Router {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
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_filter('query_vars', [$this, 'register_query_vars']);
|
||||
add_action('init', [$this, 'register_rewrite_rules']);
|
||||
add_filter('template_include', [$this, 'handle_template_include'], 99);
|
||||
add_action('init', [$this, 'maybe_flush_rewrite_rules']);
|
||||
|
||||
add_filter('login_url', [$this, 'filter_login_url'], 10, 3);
|
||||
add_filter('lostpassword_url', [$this, 'filter_lostpassword_url'], 10, 2);
|
||||
add_filter('register_url', [$this, 'filter_register_url']);
|
||||
add_filter('logout_url', [$this, 'filter_logout_url'], 10, 2);
|
||||
|
||||
add_action('login_init', [$this, 'redirect_native_login']);
|
||||
add_action('template_redirect', [$this, 'handle_logout_route']);
|
||||
}
|
||||
|
||||
public function register_query_vars(array $vars): array {
|
||||
$vars[] = 'aksrt_sign';
|
||||
$vars[] = 'aksrt_auth_tab';
|
||||
$vars[] = 'aksrt_logout';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
private function auth_route_map(): array {
|
||||
return [
|
||||
'login' => 'index.php?aksrt_sign=1&aksrt_auth_tab=login',
|
||||
'register' => 'index.php?aksrt_sign=1&aksrt_auth_tab=register',
|
||||
'bind-account' => 'index.php?aksrt_sign=1&aksrt_auth_tab=bind',
|
||||
'logout' => 'index.php?aksrt_logout=1',
|
||||
'reset-password' => 'index.php?aksrt_sign=1&aksrt_auth_tab=reset-password',
|
||||
'verify-email' => 'index.php?aksrt_sign=1&aksrt_auth_tab=verify-email',
|
||||
];
|
||||
}
|
||||
|
||||
public function register_rewrite_rules(): void {
|
||||
foreach ($this->auth_route_map() as $path => $query) {
|
||||
add_rewrite_rule('^' . preg_quote($path, '/') . '/?$', $query, 'top');
|
||||
}
|
||||
}
|
||||
|
||||
public function handle_template_include(string $template): string {
|
||||
if (get_query_var('aksrt_sign')) {
|
||||
// §4.1 Global sign gate: when close_sign is active, block non-logged-in access to sign page
|
||||
if (class_exists('AKSRT_Settings') && AKSRT_Settings::close_sign() && !is_user_logged_in()) {
|
||||
$tab = (string) get_query_var('aksrt_auth_tab');
|
||||
if (!in_array($tab, ['reset-password', 'verify-email', 'bind-account'], true) && empty($_REQUEST['interim-login'])) {
|
||||
if (!headers_sent()) {
|
||||
wp_safe_redirect(home_url('/'), 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$auth_template = function_exists('aksrt_get_auth_template_file')
|
||||
? aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true)
|
||||
: 'templates/pages/page-sign.php';
|
||||
$tpl = aksrt_locate_template_file($auth_template);
|
||||
if ($tpl) {
|
||||
return $tpl;
|
||||
}
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
public function maybe_flush_rewrite_rules(): void {
|
||||
$rewrite_version = AKSRT_VERSION . ':sign-router-v1';
|
||||
if (get_option('aksrt_auth_routes_flushed') !== $rewrite_version) {
|
||||
flush_rewrite_rules(false);
|
||||
update_option('aksrt_auth_routes_flushed', $rewrite_version);
|
||||
}
|
||||
}
|
||||
|
||||
public function filter_login_url(string $login_url, string $redirect, bool $force_reauth): string {
|
||||
if (strpos($login_url, 'wp-login.php') === false) {
|
||||
return $login_url;
|
||||
}
|
||||
|
||||
$custom_login_url = aksrt_get_login_url();
|
||||
if ($redirect !== '') {
|
||||
$custom_login_url = add_query_arg('redirect_to', urlencode($redirect), $custom_login_url);
|
||||
}
|
||||
if ($force_reauth) {
|
||||
$custom_login_url = add_query_arg('reauth', '1', $custom_login_url);
|
||||
}
|
||||
|
||||
return $custom_login_url;
|
||||
}
|
||||
|
||||
public function filter_lostpassword_url(string $lostpassword_url, string $redirect): string {
|
||||
$custom_url = aksrt_get_reset_password_url();
|
||||
if ($redirect !== '') {
|
||||
$custom_url = add_query_arg('redirect_to', urlencode($redirect), $custom_url);
|
||||
}
|
||||
return $custom_url;
|
||||
}
|
||||
|
||||
public function filter_register_url(string $register_url): string {
|
||||
return aksrt_get_register_url();
|
||||
}
|
||||
|
||||
public function filter_logout_url(string $logout_url, string $redirect): string {
|
||||
return aksrt_get_logout_url($redirect ?: home_url('/'));
|
||||
}
|
||||
|
||||
public function redirect_native_login(): void {
|
||||
if (wp_doing_ajax()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// §4.1 Emergency bypass key (admin_sign_key): allow wp-login.php direct access when key matches
|
||||
if (isset($_GET['aksrt_bypass'])) {
|
||||
$key = class_exists('AKSRT_Settings') ? AKSRT_Settings::admin_sign_key() : '';
|
||||
if ($key !== '' && hash_equals($key, (string) $_GET['aksrt_bypass'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$action = isset($_REQUEST['action']) ? sanitize_key((string) $_REQUEST['action']) : 'login';
|
||||
$redirect_to = isset($_REQUEST['redirect_to']) ? wp_unslash((string) $_REQUEST['redirect_to']) : '';
|
||||
$interim = !empty($_REQUEST['interim-login']);
|
||||
$reauth = !empty($_REQUEST['reauth']);
|
||||
$is_admin_target = $redirect_to !== '' && strpos($redirect_to, admin_url()) === 0;
|
||||
|
||||
if (is_user_logged_in() && current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($is_admin_target || $interim || $reauth || $action === 'logout') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'register':
|
||||
$target = aksrt_get_register_url();
|
||||
break;
|
||||
case 'lostpassword':
|
||||
$target = aksrt_get_reset_password_url();
|
||||
break;
|
||||
case 'rp':
|
||||
case 'resetpass':
|
||||
$target = add_query_arg('legacy', '1', aksrt_get_reset_password_url());
|
||||
break;
|
||||
default:
|
||||
$target = aksrt_get_login_url();
|
||||
break;
|
||||
}
|
||||
|
||||
$passthrough = [];
|
||||
if ($redirect_to !== '') {
|
||||
$passthrough['redirect_to'] = $redirect_to;
|
||||
}
|
||||
if ($reauth) {
|
||||
$passthrough['reauth'] = '1';
|
||||
}
|
||||
if (!empty($_REQUEST['obk'])) {
|
||||
$passthrough['obk'] = sanitize_text_field((string) $_REQUEST['obk']);
|
||||
}
|
||||
|
||||
if (!empty($passthrough)) {
|
||||
$target = add_query_arg($passthrough, $target);
|
||||
}
|
||||
|
||||
wp_safe_redirect($target, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_logout_route(): void {
|
||||
if (!get_query_var('aksrt_logout')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_user_logged_in()) {
|
||||
wp_safe_redirect(home_url('/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$nonce = isset($_GET['_wpnonce']) ? sanitize_text_field((string) $_GET['_wpnonce']) : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt-logout')) {
|
||||
wp_die(__('退出登录请求无效,请重试。', 'aksrt'));
|
||||
}
|
||||
|
||||
$redirect_to = isset($_GET['redirect_to']) ? wp_unslash((string) $_GET['redirect_to']) : home_url('/');
|
||||
wp_logout();
|
||||
wp_safe_redirect($redirect_to ?: home_url('/'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Auth Service
|
||||
*
|
||||
* 统一处理主题内认证业务:
|
||||
* - 注册时直接设置密码
|
||||
* - 主题自己的忘记密码 token 机制
|
||||
* - 重置密码
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class AKSRT_Auth_Service {
|
||||
use AKSRT_Auth_Service_Part_1a;
|
||||
use AKSRT_Auth_Service_Part_1b;
|
||||
|
||||
|
||||
private const RESET_TOKEN_HASH_META = 'aksrt_password_reset_token_hash';
|
||||
private const RESET_TOKEN_EXPIRES_META = 'aksrt_password_reset_expires';
|
||||
private const RESET_TOKEN_REQUESTED_AT = 'aksrt_password_reset_requested_at';
|
||||
private const RESET_TOKEN_TTL = 3600;
|
||||
public const EMAIL_VERIFIED_META = 'aksrt_email_verified';
|
||||
/**
|
||||
* 用户当时被发送的验证方式('link' | 'code')。
|
||||
*
|
||||
* 注意:这是 *审计字段*,仅用于后台展示「上一次给该用户发的是哪种验证邮件」。
|
||||
* 路由分支(验证页应该显示链接提示还是验证码输入框、自助/管理员重发使用哪种方式)
|
||||
* 一律读取全局配置 self::current_registration_verification_method(),
|
||||
* 这样管理员把站点从 code 切换到 link 后,所有在途用户都立即跟随新方式。
|
||||
* 不要根据这个 meta 决定页面模式或重发方式。
|
||||
*/
|
||||
public const EMAIL_METHOD_META = 'aksrt_email_verification_method';
|
||||
public const EMAIL_TOKEN_HASH_META = 'aksrt_email_verification_token_hash';
|
||||
public const EMAIL_CODE_HASH_META = 'aksrt_email_verification_code_hash';
|
||||
public const EMAIL_EXPIRES_META = 'aksrt_email_verification_expires';
|
||||
public const EMAIL_REQUESTED_AT_META = 'aksrt_email_verification_requested_at';
|
||||
public const EMAIL_TARGET_META = 'aksrt_email_verification_email';
|
||||
public const EMAIL_OBK_META = 'aksrt_email_verification_obk';
|
||||
private const EMAIL_ACCESS_KEY_META = 'aksrt_email_verification_access_key';
|
||||
public const EMAIL_CODE_ATTEMPTS_META = 'aksrt_email_verification_code_attempts';
|
||||
public const EMAIL_CODE_MAX_ATTEMPTS = 5;
|
||||
public const EMAIL_CODE_IP_TTL = 900;
|
||||
// 自助重发的 IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次。
|
||||
private const RESEND_IP_MAX = 6;
|
||||
private const RESEND_IP_WINDOW = 600;
|
||||
|
||||
/**
|
||||
* 注册流程内的"待消费"邀请码上下文。
|
||||
*
|
||||
* 由 register_user() 在 wp_create_user 之前写入,由 AKSRT_Invite::on_user_register
|
||||
* 通过 take_pending_invite_context() 一次性消费。
|
||||
*
|
||||
* 设计为请求级静态:避免污染 transient 表,也避免依赖 cookie 在跨标签场景下的不确定性。
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $pending_invite_inviter_id = 0;
|
||||
/** @var string */
|
||||
private static $pending_invite_code = '';
|
||||
|
||||
/**
|
||||
* 取出并清空当前注册请求的"表单 invite_code 解析结果"。
|
||||
*
|
||||
* @return array{inviter_id:int, code:string}
|
||||
*/
|
||||
public static function take_pending_invite_context(): array {
|
||||
$ctx = [
|
||||
'inviter_id' => (int) self::$pending_invite_inviter_id,
|
||||
'code' => (string) self::$pending_invite_code,
|
||||
];
|
||||
self::$pending_invite_inviter_id = 0;
|
||||
self::$pending_invite_code = '';
|
||||
return $ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动钩子:注册全局 authenticate 拦截器(阶段 4)
|
||||
* 调用方:bootstrap.php 在所有类加载完成后调用一次。
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter('authenticate', [self::class, 'enforce_email_verification_on_authenticate'], 30, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局登录拦截器:堵住 wp-login.php / REST / XML-RPC / Application Password 等所有
|
||||
* 不经过 AKSRT_Auth_Service::login_user() 的认证路径。
|
||||
*
|
||||
* 多重短路保护,按"成本最低"顺序排列:
|
||||
* 1. 紧急降级常量 AKSRT_NO_EMAIL_GATE
|
||||
* 2. $user 不是 WP_User(认证失败 / 未开始)
|
||||
* 3. WP_Error(前置错误,原样返回)
|
||||
* 4. 功能未启用
|
||||
* 5. 管理员豁免(manage_options)
|
||||
* 6. cron / 安装 / 升级豁免
|
||||
* 7. REST/XML-RPC 阶段化(option aksrt_email_verification_rest_block_at 控制)
|
||||
* 8. 自定义豁免过滤器
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return mixed
|
||||
*/
|
||||
public static function enforce_email_verification_on_authenticate($user, $username = '', $password = '') {
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
return $user;
|
||||
}
|
||||
if (!($user instanceof WP_User)) {
|
||||
return $user;
|
||||
}
|
||||
if (is_wp_error($user)) {
|
||||
return $user;
|
||||
}
|
||||
if (!class_exists('AKSRT_Settings') || !AKSRT_Settings::registration_requires_email_verification()) {
|
||||
return $user;
|
||||
}
|
||||
if (user_can($user, 'manage_options')) {
|
||||
return $user;
|
||||
}
|
||||
if (defined('DOING_CRON') && DOING_CRON) {
|
||||
return $user;
|
||||
}
|
||||
if (defined('WP_INSTALLING') && WP_INSTALLING) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$is_rest = (defined('REST_REQUEST') && REST_REQUEST);
|
||||
$is_xmlrpc = (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST);
|
||||
$rest_block_at = (int) get_option('aksrt_email_verification_rest_block_at', 0);
|
||||
|
||||
// 阶段化:option=0 时 REST/XML-RPC 仅记录不拦截(Day 0~3 灰度期)
|
||||
if (($is_rest || $is_xmlrpc) && $rest_block_at <= 0) {
|
||||
if (!AKSRT_Auth_Email_Verification::is_email_verified((int) $user->ID)) {
|
||||
self::log_event('auth.rest_unverified', [
|
||||
'uid' => (int) $user->ID,
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
// 已验证 → 直接放行
|
||||
if (AKSRT_Auth_Email_Verification::is_email_verified((int) $user->ID)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// 自定义豁免:filter 返回 false 即放行
|
||||
$should_block = apply_filters('aksrt_email_verification_should_block', true, $user, [
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
if (!$should_block) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
self::log_event('auth.blocked', [
|
||||
'uid' => (int) $user->ID,
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
|
||||
return new WP_Error(
|
||||
'aksrt_email_unverified',
|
||||
__('账号尚未完成邮箱验证,请先完成邮箱验证后再登录。', 'aksrt'),
|
||||
[
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量事件日志(ring buffer,最近 200 条),用于健康检查路由统计。
|
||||
* 不写入文件,仅写 transient(24h TTL),不影响性能。
|
||||
*/
|
||||
public static function log_event(string $code, array $context = []): void {
|
||||
$key = 'aksrt_email_verify_log';
|
||||
$logs = get_transient($key);
|
||||
if (!is_array($logs)) {
|
||||
$logs = [];
|
||||
}
|
||||
$logs[] = [
|
||||
't' => time(),
|
||||
'c' => $code,
|
||||
'd' => $context,
|
||||
];
|
||||
// 保留最近 200 条
|
||||
if (count($logs) > 200) {
|
||||
$logs = array_slice($logs, -200);
|
||||
}
|
||||
set_transient($key, $logs, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取最近 24h 的事件日志聚合。
|
||||
*/
|
||||
public static function get_email_verification_url(int $user_id, array $args = []): string {
|
||||
if ($user_id <= 0 || !function_exists('aksrt_get_verify_email_url')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$access_key = self::get_or_issue_email_access_key($user_id);
|
||||
if ($access_key !== '') {
|
||||
$args['vk'] = $access_key;
|
||||
}
|
||||
|
||||
return aksrt_get_verify_email_url($user_id, $args);
|
||||
}
|
||||
|
||||
public static function current_registration_verification_method(): string {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return 'link';
|
||||
}
|
||||
|
||||
$method = AKSRT_Settings::registration_verification_method();
|
||||
return in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
}
|
||||
|
||||
private static function get_or_issue_email_access_key(int $user_id, bool $rotate = false): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$current = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
|
||||
if (!$rotate && $current !== '') {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$key = wp_generate_password(48, false, false);
|
||||
update_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, $key);
|
||||
return $key;
|
||||
}
|
||||
|
||||
private static function has_valid_email_access_key(int $user_id, string $access_key): bool {
|
||||
if ($user_id <= 0 || $access_key === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stored = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
|
||||
return $stored !== '' && hash_equals($stored, $access_key);
|
||||
}
|
||||
|
||||
private static function masked_email(string $email): string {
|
||||
if ($email === '' || !is_email($email) || strpos($email, '@') === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$local_visible = function_exists('mb_substr') ? mb_substr($local, 0, 2, 'UTF-8') : substr($local, 0, 2);
|
||||
$local_masked = $local_visible . str_repeat('*', max(1, strlen($local) - strlen($local_visible)));
|
||||
|
||||
$domain_parts = explode('.', $domain);
|
||||
$domain_head = $domain_parts[0] ?? '';
|
||||
$domain_visible = substr($domain_head, 0, 1);
|
||||
$domain_parts[0] = $domain_visible . str_repeat('*', max(1, strlen($domain_head) - strlen($domain_visible)));
|
||||
|
||||
return $local_masked . '@' . implode('.', $domain_parts);
|
||||
}
|
||||
|
||||
public static function get_recent_log_summary(): array {
|
||||
$logs = get_transient('aksrt_email_verify_log');
|
||||
if (!is_array($logs)) {
|
||||
return [];
|
||||
}
|
||||
$cutoff = time() - DAY_IN_SECONDS;
|
||||
$summary = [];
|
||||
foreach ($logs as $entry) {
|
||||
if (!is_array($entry) || !isset($entry['t'], $entry['c'])) continue;
|
||||
if ((int) $entry['t'] < $cutoff) continue;
|
||||
$code = (string) $entry['c'];
|
||||
$summary[$code] = ($summary[$code] ?? 0) + 1;
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
|
||||
public static function get_oauth_bind_context(string $obk): ?array {
|
||||
$obk = sanitize_text_field($obk);
|
||||
if ($obk === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = get_transient($obk);
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
public static function get_oauth_provider_name(string $provider): string {
|
||||
$provider_names = [
|
||||
'github' => 'GitHub',
|
||||
'gitee' => 'Gitee',
|
||||
'qq' => 'QQ',
|
||||
'google' => 'Google',
|
||||
];
|
||||
|
||||
$provider = sanitize_key($provider);
|
||||
return $provider_names[$provider] ?? ucfirst($provider);
|
||||
}
|
||||
|
||||
public static function resolve_email_to_username(string $login): string {
|
||||
$login = trim($login);
|
||||
if (is_email($login)) {
|
||||
$user = get_user_by('email', $login);
|
||||
if ($user && !is_wp_error($user)) {
|
||||
return $user->user_login;
|
||||
}
|
||||
}
|
||||
return sanitize_user($login, true);
|
||||
}
|
||||
|
||||
public static function friendly_login_error(string $raw_message): string {
|
||||
$msg_lower = wp_strip_all_tags(strtolower($raw_message));
|
||||
|
||||
if (false !== strpos($msg_lower, 'incorrect password') || false !== strpos($msg_lower, '密码不正确')) {
|
||||
return __('用户名或密码错误,请重新输入', 'aksrt');
|
||||
}
|
||||
|
||||
if (
|
||||
false !== strpos($msg_lower, 'invalid username') ||
|
||||
false !== strpos($msg_lower, 'invalid email') ||
|
||||
false !== strpos($msg_lower, '<strong>error</strong>: 该用户不存在')
|
||||
) {
|
||||
return __('账号不存在,请检查输入或先注册', 'aksrt');
|
||||
}
|
||||
|
||||
if (
|
||||
false !== strpos($msg_lower, 'empty_password') ||
|
||||
false !== strpos($msg_lower, 'empty username') ||
|
||||
false !== strpos($msg_lower, 'empty')
|
||||
) {
|
||||
return __('请填写用户名和密码', 'aksrt');
|
||||
}
|
||||
|
||||
return sprintf(__('登录失败:%s', 'aksrt'), wp_strip_all_tags($raw_message));
|
||||
}
|
||||
|
||||
public static function validate_password(string $password, string $confirm = '') {
|
||||
if ($password === '') {
|
||||
return __('请输入密码', 'aksrt');
|
||||
}
|
||||
|
||||
if (mb_strlen($password) < 6) {
|
||||
return __('密码长度至少6位', 'aksrt');
|
||||
}
|
||||
|
||||
if (mb_strlen($password) > 128) {
|
||||
return __('密码过长,请控制在128位以内', 'aksrt');
|
||||
}
|
||||
|
||||
if ($confirm !== '' && $password !== $confirm) {
|
||||
return __('两次输入的密码不一致', 'aksrt');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function login_user(array $post, bool $is_ajax = false, bool $skip_nonce = false): array {
|
||||
$resolved_login = self::resolve_email_to_username((string) ($post['log'] ?? ''));
|
||||
|
||||
if (!$skip_nonce) {
|
||||
if ($is_ajax) {
|
||||
if (!wp_verify_nonce($post['_ajax_nonce'] ?? '', 'aksrt_ajax_login')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
if (!wp_verify_nonce($post['_login_nonce'] ?? '', 'aksrt_login')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$creds = [
|
||||
'user_login' => $resolved_login,
|
||||
'user_password' => (string) ($post['pwd'] ?? ''),
|
||||
'remember' => !empty($post['rememberme']),
|
||||
];
|
||||
|
||||
$original_post = $_POST;
|
||||
$_POST = array_merge($_POST, $post);
|
||||
try {
|
||||
$user = wp_signon($creds);
|
||||
} finally {
|
||||
$_POST = $original_post;
|
||||
}
|
||||
|
||||
if (is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
|
||||
}
|
||||
|
||||
// REST API 上下文中 wp_signon 可能无法正确设置 Cookie
|
||||
// 手动调用 wp_set_auth_cookie 确保 Cookie 被设置
|
||||
if (defined('REST_REQUEST') && REST_REQUEST) {
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
wp_set_current_user($user->ID);
|
||||
}
|
||||
|
||||
if (self::user_requires_email_verification($user)) {
|
||||
wp_logout();
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => __('账号尚未完成邮箱验证,请先验证邮箱后再登录', 'aksrt'),
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
];
|
||||
}
|
||||
|
||||
$redirect = !empty($post['redirect_to']) ? wp_unslash((string) $post['redirect_to']) : home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user' => $user,
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Category Rewrite — Remove /category/ from URLs
|
||||
*
|
||||
* Rewrites category URLs from /category/slug/ to /slug/.
|
||||
* Adds 301 redirect for old /category/ URLs.
|
||||
*
|
||||
* WARNING: May conflict with other "no-category" plugins.
|
||||
* Enable via Settings → SEO → Remove category base.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Category_Rewrite {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
private function rewrite_version(): string {
|
||||
return AKSRT_VERSION . ':category-rewrite-v1';
|
||||
}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!$this->is_enabled()) {
|
||||
// If disabled, ensure old rules are cleaned up
|
||||
add_action('init', [$this, 'maybe_restore']);
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter('category_link', [$this, 'remove_category_base'], 10, 2);
|
||||
add_filter('term_link', [$this, 'remove_category_base_term'], 10, 3);
|
||||
add_action('init', [$this, 'add_rewrite_rules']);
|
||||
add_action('template_redirect', [$this, 'redirect_old_urls']);
|
||||
add_action('admin_init', [$this, 'maybe_flush']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if feature is enabled
|
||||
*/
|
||||
private function is_enabled(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) return false;
|
||||
// schema key = seo_no_category; runtime alias = seo_remove_category
|
||||
return (bool) (
|
||||
AKSRT_Settings::get_value('seo_remove_category', false)
|
||||
?: AKSRT_Settings::get_value('seo_no_category', false)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove /category/ from category link
|
||||
*/
|
||||
public function remove_category_base(string $link, $term): string {
|
||||
$base = get_option('category_base');
|
||||
if ($base && $base !== 'category') return $link;
|
||||
|
||||
// Only process category taxonomy
|
||||
if ($term instanceof WP_Term && $term->taxonomy !== 'category') {
|
||||
return $link;
|
||||
}
|
||||
|
||||
$category_base_url = preg_quote(trailingslashit(home_url('/category')), '#');
|
||||
return preg_replace('#' . $category_base_url . '#', trailingslashit(home_url('/')), $link, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove base from term_link for categories
|
||||
*/
|
||||
public function remove_category_base_term(string $link, $term, $taxonomy): string {
|
||||
if ($taxonomy !== 'category') return $link;
|
||||
return $this->remove_category_base($link, $term);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rewrite rules for category URLs without /category/
|
||||
*/
|
||||
public function add_rewrite_rules(): void {
|
||||
// Get all category slugs
|
||||
$categories = get_categories(['hide_empty' => false]);
|
||||
foreach ($categories as $cat) {
|
||||
if (empty($cat->slug)) continue;
|
||||
$slug = $cat->slug;
|
||||
|
||||
// /slug/ → index.php?category_name=$slug
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($slug, '/') . '/?$',
|
||||
'index.php?category_name=' . $slug,
|
||||
'top'
|
||||
);
|
||||
|
||||
// /slug/page/N/ → index.php?category_name=$slug&paged=$N
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($slug, '/') . '/page/([0-9]+)/?$',
|
||||
'index.php?category_name=$slug&paged=$matches[1]',
|
||||
'top'
|
||||
);
|
||||
|
||||
// /slug/feed/ → index.php?category_name=$slug&feed=$feed
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($slug, '/') . '/feed/?$',
|
||||
'index.php?category_name=$slug&feed=rss2',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 301 redirect old /category/ URLs to new ones
|
||||
*/
|
||||
public function redirect_old_urls(): void {
|
||||
$request = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$home_path = wp_parse_url(home_url('/'), PHP_URL_PATH);
|
||||
|
||||
// Remove home path prefix
|
||||
if ($home_path && strpos($request, $home_path) === 0) {
|
||||
$request = substr($request, strlen($home_path));
|
||||
}
|
||||
|
||||
// Check if request starts with /category/
|
||||
if (preg_match('#^category/(.+?)/?$#', $request, $matches)) {
|
||||
$new_url = home_url('/' . $matches[1] . '/');
|
||||
wp_redirect($new_url, 301);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules when setting changes
|
||||
*/
|
||||
public function maybe_flush(): void {
|
||||
$rewrite_version = $this->rewrite_version();
|
||||
$stored = (string) get_option('aksrt_category_rewrite_flushed', '');
|
||||
if ($stored !== $rewrite_version) {
|
||||
flush_rewrite_rules();
|
||||
update_option('aksrt_category_rewrite_flushed', $rewrite_version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore default rewrite rules when feature is disabled
|
||||
*/
|
||||
public function maybe_restore(): void {
|
||||
$stored = (string) get_option('aksrt_category_rewrite_flushed', '');
|
||||
if ($stored !== '') {
|
||||
flush_rewrite_rules();
|
||||
delete_option('aksrt_category_rewrite_flushed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force flush (call after enabling/disabling)
|
||||
*/
|
||||
public static function force_flush(): void {
|
||||
flush_rewrite_rules();
|
||||
update_option('aksrt_category_rewrite_flushed', AKSRT_VERSION . ':category-rewrite-v1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Charge Card — 充值卡系统
|
||||
*
|
||||
* 管理充值卡的创建、使用和查询。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Charge_Card {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const STATUS_ACTIVE = 'active';
|
||||
const STATUS_USED = 'used';
|
||||
const STATUS_EXPIRED = 'expired';
|
||||
const STATUS_DISABLED = 'disabled';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return AKSRT_DB_Migrator::table('charge_card');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建充值卡
|
||||
*/
|
||||
public function create(array $data): int {
|
||||
global $wpdb;
|
||||
|
||||
$code = sanitize_text_field((string) ($data['code'] ?? ''));
|
||||
$amount = (float) ($data['amount'] ?? 0);
|
||||
$expires_at = sanitize_text_field((string) ($data['expires_at'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
|
||||
|
||||
if ($code === '' || $amount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 检查 code 唯一性
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE code = %s",
|
||||
$code
|
||||
));
|
||||
if ($exists) return 0;
|
||||
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'code' => $code,
|
||||
'amount' => $amount,
|
||||
'balance' => $amount,
|
||||
'status' => self::STATUS_ACTIVE,
|
||||
'expires_at' => $expires_at !== '' ? $expires_at : null,
|
||||
'created_at' => current_time('mysql'),
|
||||
'note' => $note,
|
||||
]);
|
||||
|
||||
return $result ? (int) $wpdb->insert_id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成充值卡
|
||||
*/
|
||||
public function batch_create(float $amount, int $count, int $length = 16, string $prefix = ''): array {
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = $prefix . strtoupper(wp_generate_password($length, false));
|
||||
$id = $this->create([
|
||||
'code' => $code,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
if ($id > 0) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用充值卡
|
||||
*/
|
||||
public function redeem(string $code, int $user_id): array {
|
||||
global $wpdb;
|
||||
|
||||
$code = sanitize_text_field($code);
|
||||
$card = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE code = %s",
|
||||
$code
|
||||
), ARRAY_A);
|
||||
|
||||
if (!$card) {
|
||||
return ['success' => false, 'message' => '充值卡不存在'];
|
||||
}
|
||||
|
||||
if ($card['status'] !== self::STATUS_ACTIVE) {
|
||||
return ['success' => false, 'message' => '充值卡已使用或已失效'];
|
||||
}
|
||||
|
||||
if ($card['expires_at'] && strtotime($card['expires_at']) < time()) {
|
||||
// 标记过期
|
||||
$wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $card['id']]);
|
||||
return ['success' => false, 'message' => '充值卡已过期'];
|
||||
}
|
||||
|
||||
$amount = (float) $card['balance'];
|
||||
|
||||
// 更新卡状态
|
||||
$wpdb->update($this->table(), [
|
||||
'status' => self::STATUS_USED,
|
||||
'used_by' => $user_id,
|
||||
'used_at' => current_time('mysql'),
|
||||
'balance' => 0,
|
||||
], ['id' => (int) $card['id']]);
|
||||
|
||||
// 增加用户余额
|
||||
if (class_exists('AKSRT_Credits')) {
|
||||
AKSRT_Credits::add($user_id, (int) $amount, 'charge_card', [
|
||||
'ref_type' => 'charge_card',
|
||||
'ref_id' => (int) $card['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => sprintf('充值成功,已到账 %s 元', number_format($amount, 2)),
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充值卡列表
|
||||
*/
|
||||
public function get_list(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
return [
|
||||
'items' => $items ?: [],
|
||||
'total' => $total,
|
||||
'page' => floor($offset / $limit) + 1,
|
||||
'max_pages' => ceil($total / $limit),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用充值卡
|
||||
*/
|
||||
public function disable(int $id): bool {
|
||||
global $wpdb;
|
||||
return (bool) $wpdb->update($this->table(), [
|
||||
'status' => self::STATUS_DISABLED,
|
||||
], ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
$table = $this->table();
|
||||
|
||||
$row = $wpdb->get_row(
|
||||
"SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(CASE WHEN status = 'used' THEN 1 ELSE 0 END) as used,
|
||||
SUM(amount) as total_amount,
|
||||
SUM(CASE WHEN status = 'used' THEN amount ELSE 0 END) as used_amount
|
||||
FROM {$table}",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'active' => (int) ($row['active'] ?? 0),
|
||||
'used' => (int) ($row['used'] ?? 0),
|
||||
'total_amount' => (float) ($row['total_amount'] ?? 0),
|
||||
'used_amount' => (float) ($row['used_amount'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Comment Actions
|
||||
*
|
||||
* REST endpoints for comment edit / delete / moderate.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Comment_Actions {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/comment/edit', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'edit_comment'],
|
||||
'permission_callback' => [$this, 'check_edit_permission'],
|
||||
'args' => [
|
||||
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
|
||||
'content' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/comment/delete', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'delete_comment'],
|
||||
'permission_callback' => [$this, 'check_delete_permission'],
|
||||
'args' => [
|
||||
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/comment/moderate', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'moderate_comment'],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can('moderate_comments');
|
||||
},
|
||||
'args' => [
|
||||
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
|
||||
'status' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/comment/pin', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'pin_comment'],
|
||||
'permission_callback' => [$this, 'check_pin_permission'],
|
||||
'args' => [
|
||||
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
|
||||
'pinned' => ['required' => false, 'type' => 'boolean'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/comment/like', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'like_comment'],
|
||||
// Guests may like; abuse is bounded by per-visitor toggle state.
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
|
||||
'active' => ['required' => false, 'type' => 'boolean'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the per-comment action contract (single source of truth).
|
||||
*
|
||||
* @param int $comment_id Comment ID.
|
||||
* @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool}
|
||||
*/
|
||||
private function permissions(int $comment_id): array {
|
||||
if (function_exists('aksrt_comment_action_permissions')) {
|
||||
return aksrt_comment_action_permissions($comment_id);
|
||||
}
|
||||
|
||||
// Defensive fallback if the helper is unavailable.
|
||||
$comment = get_comment($comment_id);
|
||||
$is_mod = current_user_can('moderate_comments');
|
||||
$is_owner = $comment && is_user_logged_in() && (int) $comment->user_id === get_current_user_id();
|
||||
return [
|
||||
'can_edit' => $is_mod || $is_owner,
|
||||
'can_delete' => $is_mod || $is_owner,
|
||||
'can_moderate' => $is_mod,
|
||||
'can_pin' => $is_mod,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission: can edit? (author within configured window, or moderator)
|
||||
*/
|
||||
public function check_edit_permission(\WP_REST_Request $request): bool {
|
||||
if (!is_user_logged_in()) return false;
|
||||
return $this->permissions((int) $request->get_param('comment_id'))['can_edit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission: can delete? (author, or moderator)
|
||||
*/
|
||||
public function check_delete_permission(\WP_REST_Request $request): bool {
|
||||
if (!is_user_logged_in()) return false;
|
||||
return $this->permissions((int) $request->get_param('comment_id'))['can_delete'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission: can pin? (moderator, top-level only)
|
||||
*/
|
||||
public function check_pin_permission(\WP_REST_Request $request): bool {
|
||||
if (!is_user_logged_in()) return false;
|
||||
return $this->permissions((int) $request->get_param('comment_id'))['can_pin'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit comment content
|
||||
*/
|
||||
public function edit_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = $request->get_param('comment_id');
|
||||
$content = $request->get_param('content');
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'comment_not_found',
|
||||
'message' => __('评论不存在。', 'aksrt'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Sanitize with wp_kses (allow basic HTML)
|
||||
$allowed_html = [
|
||||
'a' => ['href' => [], 'title' => [], 'target' => []],
|
||||
'code' => [],
|
||||
'pre' => [],
|
||||
'strong' => [],
|
||||
'em' => [],
|
||||
'br' => [],
|
||||
'p' => [],
|
||||
];
|
||||
$clean_content = wp_kses($content, $allowed_html);
|
||||
|
||||
$updated = wp_update_comment([
|
||||
'comment_ID' => $comment_id,
|
||||
'comment_content' => $clean_content,
|
||||
]);
|
||||
|
||||
if (is_wp_error($updated)) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'update_failed',
|
||||
'message' => __('更新失败。', 'aksrt'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'code' => 'updated',
|
||||
'message' => __('评论已更新。', 'aksrt'),
|
||||
'data' => [
|
||||
'content' => $clean_content,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete comment
|
||||
*/
|
||||
public function delete_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = $request->get_param('comment_id');
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'comment_not_found',
|
||||
'message' => __('评论不存在。', 'aksrt'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$deleted = wp_delete_comment($comment_id, false); // soft delete
|
||||
if (!$deleted) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'delete_failed',
|
||||
'message' => __('删除失败。', 'aksrt'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'code' => 'deleted',
|
||||
'message' => __('评论已删除。', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderate comment (approve / hold / spam)
|
||||
*/
|
||||
public function moderate_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = $request->get_param('comment_id');
|
||||
$status = $request->get_param('status');
|
||||
|
||||
$allowed_statuses = ['approve', 'hold', 'spam'];
|
||||
if (!in_array($status, $allowed_statuses, true)) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'invalid_status',
|
||||
'message' => __('无效状态。', 'aksrt'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'comment_not_found',
|
||||
'message' => __('评论不存在。', 'aksrt'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$status_map = [
|
||||
'approve' => 'approved',
|
||||
'hold' => 'hold',
|
||||
'spam' => 'spam',
|
||||
];
|
||||
|
||||
$result = wp_set_comment_status($comment_id, $status_map[$status]);
|
||||
if (is_wp_error($result)) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'moderate_failed',
|
||||
'message' => __('操作失败。', 'aksrt'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'code' => 'moderated',
|
||||
'message' => __('操作成功。', 'aksrt'),
|
||||
'data' => [
|
||||
'status' => $status_map[$status],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a comment's pinned ("置顶") state. Top-level comments only.
|
||||
*/
|
||||
public function pin_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = (int) $request->get_param('comment_id');
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'comment_not_found',
|
||||
'message' => __('评论不存在。', 'aksrt'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ((int) $comment->comment_parent !== 0) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'cannot_pin_reply',
|
||||
'message' => __('仅顶层评论可置顶。', 'aksrt'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$current = '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true);
|
||||
$target = null !== $request->get_param('pinned')
|
||||
? (bool) $request->get_param('pinned')
|
||||
: !$current;
|
||||
|
||||
if ($target) {
|
||||
update_comment_meta($comment_id, '_aksrt_comment_pinned', '1');
|
||||
} else {
|
||||
delete_comment_meta($comment_id, '_aksrt_comment_pinned');
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'code' => 'pinned',
|
||||
'message' => $target ? __('评论已置顶。', 'aksrt') : __('已取消置顶。', 'aksrt'),
|
||||
'data' => [
|
||||
'pinned' => $target,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the current visitor's like on a comment.
|
||||
*/
|
||||
public function like_comment(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$comment_id = (int) $request->get_param('comment_id');
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment || '1' !== (string) $comment->comment_approved) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'comment_unavailable',
|
||||
'message' => __('评论不存在或未通过审核。', 'aksrt'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_is_comment_liked') || !function_exists('aksrt_set_comment_like_state') || !function_exists('aksrt_update_comment_counter')) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => false,
|
||||
'code' => 'like_unavailable',
|
||||
'message' => __('点赞功能不可用。', 'aksrt'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$current = aksrt_is_comment_liked($comment_id);
|
||||
$target = null !== $request->get_param('active')
|
||||
? (bool) $request->get_param('active')
|
||||
: !$current;
|
||||
|
||||
if ($target !== $current) {
|
||||
aksrt_set_comment_like_state($comment_id, $target);
|
||||
$count = aksrt_update_comment_counter($comment_id, 'aksrt_comment_likes', $target ? 1 : -1);
|
||||
} else {
|
||||
$count = aksrt_get_comment_likes($comment_id);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'code' => 'liked',
|
||||
'data' => [
|
||||
'active' => $target,
|
||||
'count' => $count,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Content Audit Service
|
||||
*
|
||||
* 第三方内容审核统一入口。支持多 provider(百度内容审核、阿里云等)。
|
||||
* 文本审核 + 图片审核 + 日志 + 失败策略。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Content_Audit {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核文本内容
|
||||
*
|
||||
* @return array{passed: bool, reason: string, labels: array, raw: array}
|
||||
*/
|
||||
public function audit_text(string $text): array {
|
||||
$config = $this->get_config();
|
||||
|
||||
if (empty($config['content_audit_enabled']) || empty($config['content_audit_text_enabled'])) {
|
||||
return $this->ok('disabled');
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||||
$result = $this->call_text_api($provider, $text, $config);
|
||||
|
||||
if (!empty($config['content_audit_log_enabled'])) {
|
||||
$this->log_audit('text', $text, $result);
|
||||
}
|
||||
|
||||
if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
|
||||
$result['passed'] = true;
|
||||
$result['reason'] .= ' (failure_policy: allow)';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核图片
|
||||
*
|
||||
* @param string $image_path 本地文件路径或 URL
|
||||
* @return array{passed: bool, reason: string, labels: array, raw: array}
|
||||
*/
|
||||
public function audit_image(string $image_path): array {
|
||||
$config = $this->get_config();
|
||||
|
||||
if (empty($config['content_audit_enabled']) || empty($config['content_audit_image_enabled'])) {
|
||||
return $this->ok('disabled');
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||||
$result = $this->call_image_api($provider, $image_path, $config);
|
||||
|
||||
if (!empty($config['content_audit_log_enabled'])) {
|
||||
$this->log_audit('image', $image_path, $result);
|
||||
}
|
||||
|
||||
if (!$result['passed'] && ($config['content_audit_on_failure'] ?? 'allow') === 'allow') {
|
||||
$result['passed'] = true;
|
||||
$result['reason'] .= ' (failure_policy: allow)';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务运行时状态
|
||||
*/
|
||||
public function get_status(): array {
|
||||
$config = $this->get_config();
|
||||
$enabled = !empty($config['content_audit_enabled']);
|
||||
$provider = sanitize_key((string) ($config['content_audit_provider'] ?? 'baidu'));
|
||||
|
||||
$missing = [];
|
||||
if ($enabled && $provider === 'baidu') {
|
||||
if (empty($config['content_audit_api_key'])) $missing[] = 'content_audit_api_key';
|
||||
if (empty($config['content_audit_api_secret'])) $missing[] = 'content_audit_api_secret';
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'provider' => $provider,
|
||||
'configured' => $enabled && ($provider === 'null' || empty($missing)),
|
||||
'available' => $enabled,
|
||||
'verified' => !empty($config['content_audit_verified']),
|
||||
'transport' => 'http_api',
|
||||
'missing_fields' => $missing,
|
||||
'text_enabled' => !empty($config['content_audit_text_enabled']),
|
||||
'image_enabled' => !empty($config['content_audit_image_enabled']),
|
||||
'on_failure' => (string) ($config['content_audit_on_failure'] ?? 'allow'),
|
||||
'log_enabled' => !empty($config['content_audit_log_enabled']),
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Provider dispatch
|
||||
* ============================================================ */
|
||||
|
||||
private function call_text_api(string $provider, string $text, array $config): array {
|
||||
switch ($provider) {
|
||||
case 'baidu':
|
||||
return $this->baidu_text_censor($text, $config);
|
||||
default:
|
||||
return $this->local_pattern_check($text, $provider);
|
||||
}
|
||||
}
|
||||
|
||||
private function call_image_api(string $provider, string $image_path, array $config): array {
|
||||
switch ($provider) {
|
||||
case 'baidu':
|
||||
return $this->baidu_image_censor($image_path, $config);
|
||||
default:
|
||||
return $this->ok('no_image_provider');
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 百度内容审核 Provider
|
||||
* ============================================================ */
|
||||
|
||||
/**
|
||||
* 百度文本审核
|
||||
*/
|
||||
private function baidu_text_censor(string $text, array $config): array {
|
||||
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
|
||||
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
|
||||
if ($api_key === '' || $api_secret === '') {
|
||||
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
|
||||
if ($token === '') {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
$censor_url = trim((string) ($config['content_audit_baidu_text_url'] ?? ''));
|
||||
if ($censor_url === '') {
|
||||
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
|
||||
}
|
||||
|
||||
$request_url = add_query_arg(['access_token' => $token], $censor_url);
|
||||
$response = wp_remote_post($request_url, [
|
||||
'timeout' => 15,
|
||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||||
'body' => http_build_query(['text' => $text]),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
|
||||
}
|
||||
|
||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||||
if ($status_code !== 200) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
|
||||
}
|
||||
|
||||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($payload) || isset($payload['error_code'])) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
|
||||
}
|
||||
|
||||
return $this->parse_baidu_result($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度图片审核
|
||||
*/
|
||||
private function baidu_image_censor(string $image_path, array $config): array {
|
||||
$api_key = trim((string) ($config['content_audit_api_key'] ?? ''));
|
||||
$api_secret = trim((string) ($config['content_audit_api_secret'] ?? ''));
|
||||
if ($api_key === '' || $api_secret === '') {
|
||||
return ['passed' => false, 'reason' => 'config_missing', 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
$token = $this->baidu_get_access_token($api_key, $api_secret, $config);
|
||||
if ($token === '') {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
// 支持本地路径和 URL
|
||||
if (filter_var($image_path, FILTER_VALIDATE_URL)) {
|
||||
$image_data = @file_get_contents($image_path);
|
||||
} else {
|
||||
$image_data = @file_get_contents($image_path);
|
||||
}
|
||||
if ($image_data === false) {
|
||||
return ['passed' => false, 'reason' => 'file_error', 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
$censor_url = trim((string) ($config['content_audit_baidu_img_url'] ?? ''));
|
||||
if ($censor_url === '') {
|
||||
$censor_url = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
|
||||
}
|
||||
|
||||
$request_url = add_query_arg(['access_token' => $token], $censor_url);
|
||||
$response = wp_remote_post($request_url, [
|
||||
'timeout' => 30,
|
||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||||
'body' => http_build_query(['image' => base64_encode($image_data)]),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['error' => $response->get_error_message()]];
|
||||
}
|
||||
|
||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||||
if ($status_code !== 200) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => ['status' => $status_code]];
|
||||
}
|
||||
|
||||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($payload) || isset($payload['error_code'])) {
|
||||
return ['passed' => false, 'reason' => 'api_error', 'labels' => [], 'raw' => $payload ?: []];
|
||||
}
|
||||
|
||||
return $this->parse_baidu_result($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度 access_token 获取(带缓存)
|
||||
*/
|
||||
private function baidu_get_access_token(string $api_key, string $api_secret, array $config): string {
|
||||
$cache_key = 'aksrt_baidu_token_' . md5($api_key);
|
||||
$cached = get_transient($cache_key);
|
||||
if ($cached) return (string) $cached;
|
||||
|
||||
$token_url = trim((string) ($config['content_audit_baidu_token_url'] ?? ''));
|
||||
if ($token_url === '') {
|
||||
$token_url = 'https://aip.baidubce.com/oauth/2.0/token';
|
||||
}
|
||||
|
||||
$request_url = add_query_arg([
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => $api_key,
|
||||
'client_secret' => $api_secret,
|
||||
], $token_url);
|
||||
|
||||
$response = wp_remote_get($request_url, ['timeout' => 10]);
|
||||
if (is_wp_error($response)) return '';
|
||||
|
||||
$status_code = (int) wp_remote_retrieve_response_code($response);
|
||||
if ($status_code !== 200) return '';
|
||||
|
||||
$payload = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($payload) || isset($payload['error'])) return '';
|
||||
|
||||
$token = (string) ($payload['access_token'] ?? '');
|
||||
$expires = max(3600, (int) ($payload['expires_in'] ?? 3600) - 300);
|
||||
if ($token !== '') {
|
||||
set_transient($cache_key, $token, $expires);
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析百度审核结果 → 统一格式
|
||||
*/
|
||||
private function parse_baidu_result(array $payload): array {
|
||||
$conclusion_type = isset($payload['conclusionType']) ? (int) $payload['conclusionType'] : 1;
|
||||
$conclusion = (string) ($payload['conclusion'] ?? '');
|
||||
$top_category = $this->baidu_extract_top_category($payload);
|
||||
|
||||
// conclusionType: 1=合规, 2=不合规, 3=疑似, 4=审核失败
|
||||
if ($conclusion_type === 1) {
|
||||
return [
|
||||
'passed' => true,
|
||||
'reason' => '',
|
||||
'labels' => ['compliant'],
|
||||
'raw' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
$score_map = [2 => 95, 3 => 70, 4 => 60];
|
||||
$reason_map = [2 => 'non_compliant', 3 => 'suspected', 4 => 'review_failed'];
|
||||
|
||||
return [
|
||||
'passed' => false,
|
||||
'reason' => $reason_map[$conclusion_type] ?? 'unknown',
|
||||
'labels' => [$top_category ?: ('baidu_type_' . $conclusion_type)],
|
||||
'raw' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
private function baidu_extract_top_category(array $payload): string {
|
||||
if (!isset($payload['data']) || !is_array($payload['data']) || empty($payload['data'][0])) {
|
||||
return '';
|
||||
}
|
||||
$first = $payload['data'][0];
|
||||
if (!is_array($first)) return '';
|
||||
if (!empty($first['msg'])) return (string) $first['msg'];
|
||||
if (!empty($first['type'])) return 'type_' . (string) $first['type'];
|
||||
if (!empty($first['subType'])) return 'sub_' . (string) $first['subType'];
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 本地基础过滤(无 API 时的 fallback)
|
||||
* ============================================================ */
|
||||
|
||||
private function local_pattern_check(string $text, string $provider): array {
|
||||
$forbidden_patterns = ['/赌博/i', '/色情/i', '/枪支/i', '/毒品/i'];
|
||||
foreach ($forbidden_patterns as $pattern) {
|
||||
if (preg_match($pattern, $text, $m)) {
|
||||
return [
|
||||
'passed' => false,
|
||||
'reason' => sprintf('本地敏感词匹配: %s', $m[0]),
|
||||
'labels' => ['forbidden'],
|
||||
'raw' => ['provider' => $provider, 'method' => 'local_pattern'],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $this->ok('passthrough');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
private function ok(string $reason = ''): array {
|
||||
return ['passed' => true, 'reason' => $reason, 'labels' => [], 'raw' => []];
|
||||
}
|
||||
|
||||
private function get_config(): array {
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
return AKSRT_Settings::get_content_audit_settings();
|
||||
}
|
||||
return ['content_audit_enabled' => false];
|
||||
}
|
||||
|
||||
private function log_audit(string $type, string $content, array $result): void {
|
||||
$log = get_option('aksrt_content_audit_log', []);
|
||||
$log[] = [
|
||||
'type' => $type,
|
||||
'content' => mb_substr($content, 0, 100),
|
||||
'passed' => $result['passed'],
|
||||
'reason' => $result['reason'] ?? '',
|
||||
'time' => current_time('mysql'),
|
||||
];
|
||||
if (count($log) > 500) {
|
||||
$log = array_slice($log, -500);
|
||||
}
|
||||
update_option('aksrt_content_audit_log', $log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Content Visibility
|
||||
*
|
||||
* Central access policy for shortcode-gated content and paid-content runtime.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Content_Visibility {
|
||||
|
||||
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 {
|
||||
// 短代码注册统一在 AKSRT_Shortcodes(registry);本类只提供渲染实现。
|
||||
add_action('wp_ajax_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
|
||||
add_action('wp_ajax_nopriv_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|WP_Post|null $post
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get_post_access_state($post = null, int $user_id = 0): array {
|
||||
$resource_post = get_post($post);
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
$pay_meta = function_exists('aksrt_get_pay_meta')
|
||||
? aksrt_get_pay_meta($resource_post)
|
||||
: AKSRT_Domain_Model::default_pay_meta();
|
||||
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay_meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
|
||||
$price = (float) ($pay_meta['price'] ?? 0);
|
||||
$paid_settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_paid_content_settings()
|
||||
: [
|
||||
'download_login' => true,
|
||||
'download_credit' => true,
|
||||
'download_captcha' => false,
|
||||
];
|
||||
|
||||
$state = [
|
||||
'post_id' => $resource_post instanceof WP_Post ? (int) $resource_post->ID : 0,
|
||||
'pay_type' => $pay_type,
|
||||
'price' => $price,
|
||||
'need_login' => false,
|
||||
'need_purchase' => false,
|
||||
'need_vip' => false,
|
||||
'require_captcha' => false,
|
||||
'has_paid_access' => false,
|
||||
'can_view' => true,
|
||||
'can_download' => true,
|
||||
'download_cost' => 0,
|
||||
];
|
||||
|
||||
if (!($resource_post instanceof WP_Post)) {
|
||||
$state['can_view'] = false;
|
||||
$state['can_download'] = false;
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP;
|
||||
$has_paid_order = false;
|
||||
|
||||
if ($uid > 0 && class_exists('AKSRT_Order')) {
|
||||
$has_paid_order = AKSRT_Order::get_instance()->has_paid_access(
|
||||
$uid,
|
||||
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
(int) $resource_post->ID
|
||||
);
|
||||
}
|
||||
|
||||
$has_vip_access = false;
|
||||
if ($uid > 0) {
|
||||
if (function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) {
|
||||
$has_vip_access = true;
|
||||
} elseif (function_exists('aksrt_is_vip') && $pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
|
||||
$has_vip_access = aksrt_is_vip($uid);
|
||||
}
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
|
||||
$download_cost = function_exists('aksrt_resource_download_cost')
|
||||
? (int) aksrt_resource_download_cost($resource_post)
|
||||
: (int) ceil($price);
|
||||
|
||||
$state['download_cost'] = max(0, $download_cost);
|
||||
$state['need_login'] = !empty($paid_settings['download_login']) || !empty($pay_meta['require_login']);
|
||||
$state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
|
||||
$state['need_purchase'] = !$has_vip_access && $state['download_cost'] > 0;
|
||||
$state['has_paid_access'] = $has_vip_access || $has_paid_order || !$state['need_purchase'];
|
||||
$state['can_view'] = true;
|
||||
$state['can_download'] = !$state['need_login'] || $uid > 0;
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
|
||||
$state['need_vip'] = true;
|
||||
$state['has_paid_access'] = $has_vip_access;
|
||||
$state['can_view'] = $has_vip_access;
|
||||
$state['can_download'] = false;
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
$state['need_login'] = !empty($pay_meta['require_login']);
|
||||
$state['need_purchase'] = !$has_free_access;
|
||||
$state['has_paid_access'] = $has_free_access || $has_paid_order || $has_vip_access;
|
||||
$state['can_view'] = $state['has_paid_access'] && (!$state['need_login'] || $uid > 0);
|
||||
$state['can_download'] = false;
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
public static function can_view_paid_content($post = null, int $user_id = 0): bool {
|
||||
$state = self::get_post_access_state($post, $user_id);
|
||||
return !empty($state['can_view']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|WP_Post|null $post
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function can_download_resource($post = null, int $user_id = 0) {
|
||||
$resource_post = get_post($post);
|
||||
if (!($resource_post instanceof WP_Post) || !function_exists('aksrt_is_resource_post') || !aksrt_is_resource_post($resource_post)) {
|
||||
return new WP_Error('aksrt_not_resource', __('该内容不是可下载资源。', 'aksrt'));
|
||||
}
|
||||
|
||||
$state = self::get_post_access_state($resource_post, $user_id);
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
|
||||
if (($state['pay_type'] ?? '') !== AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
|
||||
return new WP_Error('aksrt_not_downloadable', __('该内容不是可下载资源。', 'aksrt'));
|
||||
}
|
||||
|
||||
if (!empty($state['need_login']) && $uid <= 0) {
|
||||
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
|
||||
'status' => 401,
|
||||
'login_url' => wp_login_url(get_permalink($resource_post)),
|
||||
]);
|
||||
}
|
||||
|
||||
$cost = (int) ($state['download_cost'] ?? 0);
|
||||
$paid_settings = class_exists('AKSRT_Settings')
|
||||
? AKSRT_Settings::get_paid_content_settings()
|
||||
: ['download_credit' => true];
|
||||
|
||||
if (!empty($paid_settings['download_credit']) && $cost > 0) {
|
||||
$balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0;
|
||||
if ($balance < $cost) {
|
||||
return new WP_Error('aksrt_credits_insufficient', __('积分不足,无法下载。', 'aksrt'), [
|
||||
'status' => 402,
|
||||
'balance' => $balance,
|
||||
'cost' => $cost,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render_hide_shortcode(array $atts, string $content = ''): string {
|
||||
$atts = shortcode_atts([
|
||||
'mode' => 'comment',
|
||||
'message' => '',
|
||||
'pid' => get_the_ID(),
|
||||
], $atts, 'aksrt_hide');
|
||||
|
||||
$mode = sanitize_key((string) $atts['mode']);
|
||||
$pid = (int) $atts['pid'];
|
||||
$message = (string) $atts['message'];
|
||||
|
||||
if ($pid <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->is_unlocked($mode, $pid)) {
|
||||
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked">' . do_shortcode($content) . '</div>';
|
||||
}
|
||||
|
||||
if ($message === '') {
|
||||
switch ($mode) {
|
||||
case 'comment':
|
||||
$message = __('评论后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
case 'login':
|
||||
$message = __('登录后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
case 'password':
|
||||
$message = __('输入密码后查看隐藏内容', 'aksrt');
|
||||
break;
|
||||
default:
|
||||
$message = __('此内容已隐藏', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
$wrapper_class = 'aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--' . esc_attr($mode);
|
||||
$html = '<div class="' . $wrapper_class . '" data-aksrt-visibility-mode="' . esc_attr($mode) . '" data-aksrt-visibility-pid="' . esc_attr((string) $pid) . '">';
|
||||
$html .= '<div class="aksrt-hidden-content__message">';
|
||||
$html .= '<i class="fas fa-lock" aria-hidden="true"></i> ';
|
||||
$html .= '<span>' . esc_html($message) . '</span>';
|
||||
$html .= '</div>';
|
||||
|
||||
if ($mode === 'password') {
|
||||
$html .= '<div class="aksrt-hidden-content__form">';
|
||||
$html .= '<input type="password" class="aksrt-hidden-content__password-input" placeholder="' . esc_attr__('请输入密码', 'aksrt') . '">';
|
||||
$html .= '<button type="button" class="aksrt-hidden-content__submit-btn btn-primary btn-sm">' . esc_html__('验证', 'aksrt') . '</button>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function is_unlocked(string $mode, int $pid): bool {
|
||||
switch ($mode) {
|
||||
case 'comment':
|
||||
if (!is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return get_comments([
|
||||
'post_id' => $pid,
|
||||
'user_id' => get_current_user_id(),
|
||||
'count' => true,
|
||||
'status' => 'approve',
|
||||
]) > 0;
|
||||
|
||||
case 'login':
|
||||
return is_user_logged_in();
|
||||
|
||||
case 'password':
|
||||
return !empty($_COOKIE['aksrt_hide_pwd_' . $pid]);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function ajax_check_visibility(): void {
|
||||
check_ajax_referer('aksrt_visibility', '_nonce');
|
||||
|
||||
$mode = sanitize_key((string) ($_POST['mode'] ?? ''));
|
||||
$pid = (int) ($_POST['pid'] ?? 0);
|
||||
|
||||
if ($pid <= 0 || !in_array($mode, ['comment', 'login', 'password'], true)) {
|
||||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||||
}
|
||||
|
||||
if ($mode === 'password') {
|
||||
$password = sanitize_text_field((string) ($_POST['password'] ?? ''));
|
||||
$post_password = get_post_password($pid);
|
||||
if ($post_password === '' || $password !== $post_password) {
|
||||
wp_send_json_error(['message' => __('密码错误', 'aksrt')]);
|
||||
}
|
||||
|
||||
setcookie('aksrt_hide_pwd_' . $pid, '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
|
||||
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
|
||||
}
|
||||
|
||||
if ($this->is_unlocked($mode, $pid)) {
|
||||
wp_send_json_success(['message' => __('已解锁', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('未满足条件', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Coupon — 优惠券系统
|
||||
*
|
||||
* 使用专用表 {prefix}aksrt_coupon 存储优惠券。
|
||||
* 支持:创建、列表、验证、核销。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Coupon {
|
||||
|
||||
const TYPE_PERCENT = 'percent';
|
||||
const TYPE_FIXED = 'fixed';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return AKSRT_DB_Migrator::table('coupon');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建优惠券
|
||||
*/
|
||||
public function create(array $data): int {
|
||||
global $wpdb;
|
||||
|
||||
$code = sanitize_text_field((string) ($data['code'] ?? ''));
|
||||
if ($code === '') return 0;
|
||||
|
||||
// Check uniqueness
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE code = %s",
|
||||
$code
|
||||
));
|
||||
if ($exists) return 0;
|
||||
|
||||
$type = sanitize_key((string) ($data['type'] ?? self::TYPE_FIXED));
|
||||
if (!in_array($type, [self::TYPE_PERCENT, self::TYPE_FIXED], true)) {
|
||||
$type = self::TYPE_FIXED;
|
||||
}
|
||||
|
||||
$wpdb->insert($this->table(), [
|
||||
'code' => $code,
|
||||
'type' => $type,
|
||||
'value' => (float) ($data['value'] ?? 0),
|
||||
'min_amount' => (float) ($data['min_amount'] ?? 0),
|
||||
'usage_limit' => (int) ($data['usage_limit'] ?? 0),
|
||||
'used_count' => 0,
|
||||
'expires_at' => sanitize_text_field((string) ($data['expires_at'] ?? '')),
|
||||
'per_user_limit' => (int) ($data['per_user_limit'] ?? 0),
|
||||
'product_scope' => wp_json_encode($data['product_scope'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'max_discount' => (float) ($data['max_discount'] ?? 0),
|
||||
'status' => 'active',
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%s', '%s', '%f', '%f', '%d', '%d', '%s', '%d', '%s', '%f', '%s', '%s']);
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优惠券列表
|
||||
*/
|
||||
public function get_list(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$search = sanitize_text_field((string) ($args['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
$where[] = 'code LIKE %s';
|
||||
$params[] = '%' . $wpdb->esc_like($search) . '%';
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||
if (!empty($params)) {
|
||||
$total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
|
||||
} else {
|
||||
$total = (int) $wpdb->get_var($count_sql);
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
return [
|
||||
'items' => array_map([$this, 'normalize_row'], (array) $rows),
|
||||
'total' => $total,
|
||||
'page' => max(1, (int) ceil($offset / $limit) + 1),
|
||||
'total_pages' => max(1, (int) ceil($total / $limit)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证优惠券是否可用
|
||||
*
|
||||
* @return array{valid: bool, coupon?: array, message?: string}
|
||||
*/
|
||||
public function validate(string $code, float $order_amount = 0, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$code = sanitize_text_field($code);
|
||||
if ($code === '') {
|
||||
return ['valid' => false, 'message' => '请输入优惠码'];
|
||||
}
|
||||
|
||||
$coupon = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE code = %s",
|
||||
$code
|
||||
), ARRAY_A);
|
||||
|
||||
if (!$coupon) {
|
||||
return ['valid' => false, 'message' => '优惠码不存在'];
|
||||
}
|
||||
|
||||
$coupon = $this->normalize_row($coupon);
|
||||
|
||||
if ($coupon['status'] !== 'active') {
|
||||
return ['valid' => false, 'message' => '优惠码已失效'];
|
||||
}
|
||||
|
||||
if (!empty($coupon['expires_at']) && strtotime($coupon['expires_at']) < time()) {
|
||||
return ['valid' => false, 'message' => '优惠码已过期'];
|
||||
}
|
||||
|
||||
if ($coupon['usage_limit'] > 0 && $coupon['used_count'] >= $coupon['usage_limit']) {
|
||||
return ['valid' => false, 'message' => '优惠码已用完'];
|
||||
}
|
||||
|
||||
if ($coupon['min_amount'] > 0 && $order_amount < $coupon['min_amount']) {
|
||||
return ['valid' => false, 'message' => '订单金额不满足最低消费要求'];
|
||||
}
|
||||
|
||||
// Per-user limit check
|
||||
$user_id = (int) ($args['user_id'] ?? 0);
|
||||
if ($coupon['per_user_limit'] > 0 && $user_id > 0) {
|
||||
$user_used = $this->get_user_usage_count($coupon['id'], $user_id);
|
||||
if ($user_used >= $coupon['per_user_limit']) {
|
||||
return ['valid' => false, 'message' => '您已使用该优惠码达到上限'];
|
||||
}
|
||||
}
|
||||
|
||||
// Product scope check
|
||||
if (!empty($coupon['product_scope']) && !empty($args['product_ids'])) {
|
||||
$scope = is_array($coupon['product_scope']) ? $coupon['product_scope'] : json_decode($coupon['product_scope'], true);
|
||||
if (is_array($scope) && !empty($scope)) {
|
||||
$scope_type = (string) ($scope['type'] ?? '');
|
||||
$scope_ids = array_map('intval', (array) ($scope['ids'] ?? []));
|
||||
$product_ids = array_map('intval', (array) $args['product_ids']);
|
||||
if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) {
|
||||
return ['valid' => false, 'message' => '该优惠码不适用于当前商品'];
|
||||
}
|
||||
if ($scope_type === 'exclude' && !empty(array_intersect($product_ids, $scope_ids))) {
|
||||
return ['valid' => false, 'message' => '该优惠码不适用于当前商品'];
|
||||
}
|
||||
if ($scope_type === 'category') {
|
||||
$matched = false;
|
||||
foreach ($product_ids as $pid) {
|
||||
$cats = wp_get_post_terms($pid, 'aksrt_product_cat', ['fields' => 'ids']);
|
||||
if (!is_wp_error($cats) && !empty(array_intersect($cats, $scope_ids))) {
|
||||
$matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matched) {
|
||||
return ['valid' => false, 'message' => '该优惠码不适用于当前商品分类'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate discount
|
||||
$discount = 0;
|
||||
if ($coupon['type'] === self::TYPE_PERCENT) {
|
||||
$discount = $order_amount * ($coupon['value'] / 100);
|
||||
} else {
|
||||
$discount = min($coupon['value'], $order_amount);
|
||||
}
|
||||
|
||||
// Max discount cap
|
||||
if ($coupon['max_discount'] > 0 && $discount > $coupon['max_discount']) {
|
||||
$discount = $coupon['max_discount'];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'coupon' => $coupon,
|
||||
'discount' => round($discount, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销优惠券
|
||||
*/
|
||||
public function use_coupon(string $code): bool {
|
||||
global $wpdb;
|
||||
|
||||
$code = sanitize_text_field($code);
|
||||
$result = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET used_count = used_count + 1 WHERE code = %s",
|
||||
$code
|
||||
));
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用优惠券
|
||||
*/
|
||||
public function disable(int $id): bool {
|
||||
global $wpdb;
|
||||
return $wpdb->update($this->table(), ['status' => 'disabled'], ['id' => $id], ['%s'], ['%d']) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many times a user has used a specific coupon.
|
||||
*/
|
||||
public function get_user_usage_count(int $coupon_id, int $user_id): int {
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('coupon_usage');
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE coupon_id = %d AND user_id = %d",
|
||||
$coupon_id, $user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a coupon usage record.
|
||||
*/
|
||||
public function log_usage(int $coupon_id, int $user_id, string $order_number, float $discount): void {
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('coupon_usage');
|
||||
$wpdb->insert($table, [
|
||||
'coupon_id' => $coupon_id,
|
||||
'user_id' => $user_id,
|
||||
'order_number' => $order_number,
|
||||
'discount' => $discount,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%d', '%s', '%f', '%s']);
|
||||
}
|
||||
|
||||
private function normalize_row(array $row): array {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['value'] = (float) $row['value'];
|
||||
$row['min_amount'] = (float) $row['min_amount'];
|
||||
$row['usage_limit'] = (int) $row['usage_limit'];
|
||||
$row['used_count'] = (int) $row['used_count'];
|
||||
$row['per_user_limit'] = (int) ($row['per_user_limit'] ?? 0);
|
||||
$row['max_discount'] = (float) ($row['max_discount'] ?? 0);
|
||||
if (!empty($row['product_scope']) && is_string($row['product_scope'])) {
|
||||
$row['product_scope'] = json_decode($row['product_scope'], true) ?: [];
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Credits Earn
|
||||
*
|
||||
* 积分获取入口,挂载到 WordPress 钩子:
|
||||
* - 注册赠送 → user_register
|
||||
* - 评论奖励 → wp_insert_comment / transition_comment_status
|
||||
* - 签到/邀请 → 由 AKSRT_Signin / AKSRT_Invite 独立处理(REST)
|
||||
*
|
||||
* 所有发放都受 aksrt_credit_settings.* 子开关控制,关闭后不发放。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Credits_Earn {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// 注册赠送
|
||||
add_action('user_register', [$this, 'on_user_register'], 10, 1);
|
||||
|
||||
// 评论奖励:on_approved 时机决定挂到哪个钩子
|
||||
// 默认 on_approved=true → 仅审核通过后发;否则插入即发
|
||||
add_action('wp_insert_comment', [$this, 'on_comment_inserted'], 10, 2);
|
||||
add_action('transition_comment_status', [$this, 'on_comment_transition'], 10, 3);
|
||||
|
||||
// 论坛发帖奖励
|
||||
add_action('aksrt_forum_topic_created', [$this, 'on_forum_topic_created'], 10, 2);
|
||||
|
||||
// 论坛回复奖励(§7.7)
|
||||
add_action('comment_post', [$this, 'on_forum_reply_posted'], 20, 3);
|
||||
|
||||
// 论坛精华/热帖奖励(§7.7)
|
||||
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 论坛发帖积分奖励
|
||||
*/
|
||||
public function on_forum_topic_created(int $user_id, int $post_id): void {
|
||||
if (!class_exists('AKSRT_Credits') || !class_exists('AKSRT_Settings')) return;
|
||||
$cfg = AKSRT_Settings::get_credit_settings();
|
||||
if (empty($cfg['enabled'])) return;
|
||||
$reward = $cfg['forum_topic_reward'] ?? [];
|
||||
if (empty($reward['enabled'])) return;
|
||||
|
||||
$amount = (int) ($reward['amount'] ?? 0);
|
||||
if ($amount <= 0) return;
|
||||
|
||||
$daily_limit = (int) ($reward['daily_limit'] ?? 5);
|
||||
$today_count = $this->today_forum_reward_count($user_id, 'topic');
|
||||
if ($today_count >= $daily_limit) return;
|
||||
|
||||
AKSRT_Credits::add($user_id, $amount, 'forum_topic', ['type' => 'post', 'id' => $post_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日论坛奖励次数
|
||||
*/
|
||||
private function today_forum_reward_count(int $user_id, string $type = 'topic'): int {
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('credit_log');
|
||||
if (!$table) return 0;
|
||||
|
||||
$today = current_time('Y-m-d');
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s",
|
||||
$user_id, 'forum_' . $type, $today
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册赠送。
|
||||
*/
|
||||
public function on_user_register(int $user_id): void {
|
||||
if (!class_exists('AKSRT_Credits') || !class_exists('AKSRT_Settings')) return;
|
||||
$cfg = AKSRT_Settings::get_credit_settings();
|
||||
if (empty($cfg['enabled'])) return;
|
||||
$bonus = (int) ($cfg['register_bonus'] ?? 0);
|
||||
if ($bonus <= 0) return;
|
||||
|
||||
AKSRT_Credits::add($user_id, $bonus, 'register', ['type' => 'user', 'id' => $user_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论插入(直接发表无需审核场景)。
|
||||
*/
|
||||
public function on_comment_inserted(int $comment_id, $comment): void {
|
||||
if (!class_exists('AKSRT_Settings')) return;
|
||||
$cfg = AKSRT_Settings::get_credit_settings();
|
||||
$reward = $cfg['comment_reward'] ?? [];
|
||||
if (empty($cfg['enabled']) || empty($reward['enabled'])) return;
|
||||
|
||||
// 仅审核通过后发放 → 不在 insert 时处理
|
||||
if (!empty($reward['on_approved'])) return;
|
||||
|
||||
$comment = $comment instanceof WP_Comment ? $comment : get_comment($comment_id);
|
||||
if (!($comment instanceof WP_Comment)) return;
|
||||
|
||||
// 只奖励已批准的评论
|
||||
if ((int) $comment->comment_approved !== 1) return;
|
||||
|
||||
$this->maybe_reward_comment($comment, $reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论状态流转。
|
||||
*/
|
||||
public function on_comment_transition($new_status, $old_status, $comment): void {
|
||||
if (!class_exists('AKSRT_Settings')) return;
|
||||
$cfg = AKSRT_Settings::get_credit_settings();
|
||||
$reward = $cfg['comment_reward'] ?? [];
|
||||
if (empty($cfg['enabled']) || empty($reward['enabled'])) return;
|
||||
if (empty($reward['on_approved'])) return;
|
||||
|
||||
if ($new_status !== 'approved') return;
|
||||
// 跳过已经处理过的:以 commentmeta 标记
|
||||
if (!($comment instanceof WP_Comment)) return;
|
||||
|
||||
$this->maybe_reward_comment($comment, $reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* 真正的评论奖励逻辑(含字数 / 每日上限 / 去重)。
|
||||
*/
|
||||
private function maybe_reward_comment(WP_Comment $comment, array $reward): void {
|
||||
$user_id = (int) $comment->user_id;
|
||||
if ($user_id <= 0) return;
|
||||
|
||||
// 防重复:commentmeta 标记
|
||||
if (get_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', true)) return;
|
||||
|
||||
$min_length = (int) ($reward['min_length'] ?? 0);
|
||||
if ($min_length > 0 && mb_strlen(strip_tags($comment->comment_content)) < $min_length) return;
|
||||
|
||||
$amount = (int) ($reward['amount'] ?? 0);
|
||||
if ($amount <= 0) return;
|
||||
|
||||
// 每日上限
|
||||
$cap = (int) ($reward['daily_cap'] ?? 0);
|
||||
if ($cap > 0) {
|
||||
$today_count = $this->today_comment_reward_count($user_id);
|
||||
if ($today_count >= $cap) {
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', 'capped');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$result = AKSRT_Credits::add(
|
||||
$user_id,
|
||||
$amount,
|
||||
'comment',
|
||||
['type' => 'comment', 'id' => (int) $comment->comment_ID]
|
||||
);
|
||||
if (!is_wp_error($result)) {
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当日评论奖励次数(查询 credit_log)。
|
||||
*/
|
||||
private function today_comment_reward_count(int $user_id): int {
|
||||
global $wpdb;
|
||||
if (!class_exists('AKSRT_DB_Migrator')) return 0;
|
||||
$table = AKSRT_DB_Migrator::table('credit_log');
|
||||
if (!$table) return 0;
|
||||
|
||||
// 站点时区当日 00:00 起
|
||||
$tz = wp_timezone();
|
||||
$today = (new DateTimeImmutable('today', $tz))->format('Y-m-d H:i:s');
|
||||
// 转 UTC(数据库 created_at 用 mysql UTC)
|
||||
$today_utc = (new DateTimeImmutable('today', $tz))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
|
||||
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = 'comment' AND created_at >= %s",
|
||||
$user_id,
|
||||
$today_utc
|
||||
));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Forum credits (§7.7)
|
||||
// ================================================================
|
||||
|
||||
public function on_forum_reply_posted(int $comment_id, $approved, array $commentdata = []): void {
|
||||
if (!class_exists('AKSRT_Credits') || !class_exists('AKSRT_Settings')) return;
|
||||
if (!class_exists('AKSRT_Forum')) return;
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) return;
|
||||
|
||||
$post = get_post((int) $comment->comment_post_ID);
|
||||
if (!$post || $post->post_type !== 'aksrt_forum_topic') return;
|
||||
|
||||
if ((int) $comment->comment_approved !== 1) return;
|
||||
|
||||
$user_id = (int) $comment->user_id;
|
||||
if ($user_id <= 0) return;
|
||||
|
||||
$forum_cfg = AKSRT_Settings::get_forum_settings();
|
||||
$amount = (int) ($forum_cfg['credits']['comment_new'] ?? 2);
|
||||
if ($amount <= 0) return;
|
||||
|
||||
$daily_max = (int) ($forum_cfg['credits']['daily_max'] ?? 50);
|
||||
$today_count = $this->today_forum_reward_count($user_id, 'reply');
|
||||
if ($today_count >= $daily_max) return;
|
||||
|
||||
AKSRT_Credits::add($user_id, $amount, 'forum_reply', [
|
||||
'type' => 'comment',
|
||||
'id' => $comment_id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function on_forum_topic_featured(int $topic_id, int $operator_id): void {
|
||||
// Essence reward is handled in AKSRT_Forum::reward_essence().
|
||||
// This hook is for additional integrations if needed.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Credits Service
|
||||
*
|
||||
* 积分系统核心:余额读取、加减、日志写入。
|
||||
*
|
||||
* 数据存储:
|
||||
* - 余额:user_meta key = 'aksrt_credits' (int)
|
||||
* - 流水:表 {prefix}aksrt_credit_log
|
||||
*
|
||||
* 并发安全:
|
||||
* - 所有加减操作通过 user_meta 直读 + DB 更新,
|
||||
* 使用 wp_cache 失效 + 短事务,避免高并发下的丢失。
|
||||
* - 同一用户短时间多次扣减时仍可能出现少量不一致,
|
||||
* 可通过 P5 阶段的 token 唯一性兜底(同一资源不会被重复扣费)。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Credits {
|
||||
|
||||
/** user_meta 键名 */
|
||||
const META_KEY = 'aksrt_credits';
|
||||
|
||||
/**
|
||||
* 取用户余额。
|
||||
*/
|
||||
public static function balance(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
$val = get_user_meta($user_id, self::META_KEY, true);
|
||||
return is_numeric($val) ? (int) $val : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加积分。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $delta 必须 > 0
|
||||
* @param string $reason 例 register/signin/comment/invite/admin
|
||||
* @param array $ref ['type' => 'post|comment|user|invite', 'id' => int]
|
||||
* @return int|WP_Error 新余额或错误
|
||||
*/
|
||||
public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '') {
|
||||
if ($user_id <= 0) return new WP_Error('aksrt_credits_user', __('用户无效', 'aksrt'));
|
||||
if ($delta <= 0) return new WP_Error('aksrt_credits_delta', __('增加积分必须为正数', 'aksrt'));
|
||||
|
||||
$new_balance = self::atomic_update($user_id, $delta, false);
|
||||
if ($new_balance === null) {
|
||||
return new WP_Error('aksrt_credits_db', __('积分写入失败', 'aksrt'));
|
||||
}
|
||||
self::log($user_id, $delta, $new_balance, $reason, $ref, $remark);
|
||||
|
||||
do_action('aksrt_credits_added', $user_id, $delta, $reason, $new_balance);
|
||||
return $new_balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣减积分。
|
||||
*
|
||||
* @return int|WP_Error 新余额或错误
|
||||
*/
|
||||
public static function deduct(int $user_id, int $cost, string $reason, array $ref = [], string $remark = '') {
|
||||
if ($user_id <= 0) return new WP_Error('aksrt_credits_user', __('用户无效', 'aksrt'));
|
||||
if ($cost < 0) return new WP_Error('aksrt_credits_delta', __('扣减积分必须为非负数', 'aksrt'));
|
||||
if ($cost === 0) {
|
||||
return self::balance($user_id);
|
||||
}
|
||||
|
||||
$new_balance = self::atomic_update($user_id, $cost, true);
|
||||
if ($new_balance === null) {
|
||||
$current = self::balance($user_id);
|
||||
return new WP_Error('aksrt_credits_insufficient', __('积分不足', 'aksrt'), [
|
||||
'balance' => $current,
|
||||
'cost' => $cost,
|
||||
]);
|
||||
}
|
||||
|
||||
self::log($user_id, -$cost, $new_balance, $reason, $ref, $remark);
|
||||
|
||||
do_action('aksrt_credits_deducted', $user_id, $cost, $reason, $new_balance);
|
||||
return $new_balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子加减:用单条 SQL UPDATE 修改 user_meta,避免 read-modify-write 竞态。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $abs_delta 绝对值
|
||||
* @param bool $is_deduct true=扣减(带余额检查),false=增加
|
||||
* @return int|null 新余额;失败(余额不足或 DB 错误)返回 null
|
||||
*/
|
||||
private static function atomic_update(int $user_id, int $abs_delta, bool $is_deduct): ?int {
|
||||
global $wpdb;
|
||||
|
||||
// 确保 meta 行存在(首次扣费可能没有 row)
|
||||
$existing = get_user_meta($user_id, self::META_KEY, true);
|
||||
if ($existing === '' || $existing === false || $existing === null) {
|
||||
update_user_meta($user_id, self::META_KEY, 0);
|
||||
}
|
||||
|
||||
$meta_key = self::META_KEY;
|
||||
|
||||
if ($is_deduct) {
|
||||
// 扣减:仅当 meta_value >= cost 时才更新
|
||||
$sql = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->usermeta}
|
||||
SET meta_value = CAST(meta_value AS SIGNED) - %d
|
||||
WHERE user_id = %d
|
||||
AND meta_key = %s
|
||||
AND CAST(meta_value AS SIGNED) >= %d",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key,
|
||||
$abs_delta
|
||||
);
|
||||
} else {
|
||||
$sql = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->usermeta}
|
||||
SET meta_value = CAST(meta_value AS SIGNED) + %d
|
||||
WHERE user_id = %d
|
||||
AND meta_key = %s",
|
||||
$abs_delta,
|
||||
$user_id,
|
||||
$meta_key
|
||||
);
|
||||
}
|
||||
|
||||
$affected = $wpdb->query($sql);
|
||||
if ($affected === false || $affected === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 失效 user meta cache,下次 get_user_meta 会重读
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
|
||||
$new = (int) get_user_meta($user_id, self::META_KEY, true);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写一条流水。
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param int $delta 本次变动(正/负/0)
|
||||
* @param int $balance_after 操作后余额
|
||||
* @param string $reason
|
||||
* @param array $ref
|
||||
*/
|
||||
public static function log(int $user_id, int $delta, int $balance_after, string $reason, array $ref = [], string $remark = ''): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = AKSRT_DB_Migrator::table('credit_log');
|
||||
if (!$table) return;
|
||||
|
||||
$ref_type = isset($ref['type']) ? sanitize_key((string) $ref['type']) : null;
|
||||
$ref_id = isset($ref['id']) ? (int) $ref['id'] : null;
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'user_id' => $user_id,
|
||||
'delta' => $delta,
|
||||
'balance_after' => $balance_after,
|
||||
'reason' => substr($reason, 0, 64),
|
||||
'remark' => $remark !== '' ? substr($remark, 0, 255) : null,
|
||||
'ref_type' => $ref_type,
|
||||
'ref_id' => $ref_id,
|
||||
'ip' => self::client_ip(),
|
||||
'ua' => self::client_ua(),
|
||||
'created_at' => current_time('mysql', true),
|
||||
],
|
||||
['%d', '%d', '%d', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取流水(管理员页面用)。
|
||||
*
|
||||
* @param array $args { user_id, reason, page, per_page, date_from, date_to }
|
||||
* @return array { items, total, page, per_page }
|
||||
*/
|
||||
public static function get_log(array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = AKSRT_DB_Migrator::table('credit_log');
|
||||
if (!$table) {
|
||||
return ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
}
|
||||
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = min(100, max(1, (int) ($args['per_page'] ?? 20)));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($args['user_id'])) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = (int) $args['user_id'];
|
||||
}
|
||||
if (!empty($args['reason'])) {
|
||||
$where[] = 'reason = %s';
|
||||
$params[] = sanitize_key((string) $args['reason']);
|
||||
}
|
||||
if (!empty($args['date_from'])) {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = (string) $args['date_from'];
|
||||
}
|
||||
if (!empty($args['date_to'])) {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = (string) $args['date_to'];
|
||||
}
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
|
||||
// total
|
||||
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
$total = (int) ($params
|
||||
? $wpdb->get_var($wpdb->prepare($count_sql, $params))
|
||||
: $wpdb->get_var($count_sql));
|
||||
|
||||
// items
|
||||
$items_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d";
|
||||
$items_params = array_merge($params, [$per_page, $offset]);
|
||||
$items = $wpdb->get_results($wpdb->prepare($items_sql, $items_params), ARRAY_A);
|
||||
if (!is_array($items)) $items = [];
|
||||
|
||||
// user 名字预填
|
||||
$uid_set = array_unique(array_map(fn($r) => (int) $r['user_id'], $items));
|
||||
$names = [];
|
||||
foreach ($uid_set as $uid) {
|
||||
$u = get_userdata($uid);
|
||||
if ($u) $names[$uid] = $u->display_name;
|
||||
}
|
||||
foreach ($items as &$row) {
|
||||
$row['user_display_name'] = $names[(int) $row['user_id']] ?? '#' . $row['user_id'];
|
||||
$row['delta'] = (int) $row['delta'];
|
||||
$row['balance_after'] = (int) $row['balance_after'];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户余额列表(管理员)。
|
||||
*
|
||||
* @param array $args { search, page, per_page }
|
||||
* @return array
|
||||
*/
|
||||
public static function list_users(array $args = []): array {
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = min(100, max(1, (int) ($args['per_page'] ?? 20)));
|
||||
|
||||
$q = [
|
||||
'number' => $per_page,
|
||||
'paged' => $page,
|
||||
'orderby' => 'registered',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
if (!empty($args['search'])) {
|
||||
$q['search'] = '*' . esc_attr((string) $args['search']) . '*';
|
||||
$q['search_columns'] = ['user_login', 'user_email', 'display_name', 'user_nicename'];
|
||||
}
|
||||
$user_query = new WP_User_Query($q);
|
||||
$users = $user_query->get_results();
|
||||
$items = [];
|
||||
foreach ($users as $u) {
|
||||
$items[] = [
|
||||
'id' => (int) $u->ID,
|
||||
'login' => $u->user_login,
|
||||
'email' => $u->user_email,
|
||||
'display_name' => $u->display_name,
|
||||
'balance' => self::balance((int) $u->ID),
|
||||
'registered' => $u->user_registered,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => (int) $user_query->get_total(),
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
// ---------- 工具方法 ----------
|
||||
|
||||
private static function client_ip(): string {
|
||||
if (function_exists('aksrt_get_client_ip')) {
|
||||
return (string) aksrt_get_client_ip();
|
||||
}
|
||||
if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return (string) $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function client_ua(): string {
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
|
||||
return substr($ua, 0, 255);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT DB Migrator
|
||||
*
|
||||
* 维护 CMS / 积分 / 下载 token 等运行时所需的数据表。
|
||||
*
|
||||
* 使用 dbDelta 幂等创建,所有表带主题前缀 aksrt_。
|
||||
*
|
||||
* 表清单(v3.5 新增):
|
||||
* - {prefix}aksrt_credit_log 积分流水
|
||||
* - {prefix}aksrt_signin_log 每日签到记录
|
||||
* - {prefix}aksrt_invite 邀请关系
|
||||
* - {prefix}aksrt_dl_token 下载短期 token
|
||||
* - {prefix}aksrt_notification 用户通知收件箱
|
||||
*
|
||||
* 触发方式:
|
||||
* 1. switch_theme / after_switch_theme 钩子
|
||||
* 2. 检测 option 'aksrt_db_schema_version' 与 AKSRT_DB_SCHEMA_VERSION 不一致
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_DB_Migrator {
|
||||
use AKSRT_DB_Migrator_Part_1;
|
||||
use AKSRT_DB_Migrator_Part_2;
|
||||
use AKSRT_DB_Migrator_Part_3;
|
||||
|
||||
|
||||
/** 当前 schema 版本,更改后会触发 dbDelta。 */
|
||||
const SCHEMA_VERSION = '1.13.0';
|
||||
|
||||
const OPT_KEY = 'aksrt_db_schema_version';
|
||||
|
||||
/**
|
||||
* 初始化钩子。
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
|
||||
add_action('admin_init', [self::class, 'maybe_migrate']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若版本不一致则执行迁移。
|
||||
*/
|
||||
public static function maybe_migrate(): void {
|
||||
$current = get_option(self::OPT_KEY, '');
|
||||
if ($current !== self::SCHEMA_VERSION) {
|
||||
self::run();
|
||||
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
// 选项级(option)数据迁移:独立版本号控制,与表结构解耦。
|
||||
self::maybe_migrate_options();
|
||||
}
|
||||
|
||||
const OPT_LAYOUT_MIGRATED = 'aksrt_archive_layout_migrated';
|
||||
const OPT_RESOURCE_MIGRATED = 'aksrt_resource_cpt_migrated';
|
||||
const OPT_CMS_FORMAT_MIGRATED = 'aksrt_cms_format_migrated';
|
||||
const OPT_RESOURCE_PAY_META_CURSOR = 'aksrt_resource_pay_meta_cursor';
|
||||
const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated';
|
||||
|
||||
/**
|
||||
* 一次性 option 迁移:
|
||||
* - 旧 aksrt_basic_settings.post_layout / aksrt_settings.default_archive_layout
|
||||
* → 统一为 aksrt_settings.archive_layout
|
||||
* - 旧 aksrt_resource CPT 文章 → post + _aksrt_is_resource = 1
|
||||
* - v4 形态归一:清理 site_profile / opening_mode / archive_layout 等历史字段
|
||||
* 并删除全部分类的 aksrt_flow term meta
|
||||
*/
|
||||
public static function maybe_migrate_options(): void {
|
||||
self::maybe_migrate_layout();
|
||||
self::maybe_migrate_resource_cpt();
|
||||
self::maybe_migrate_cms_format();
|
||||
self::maybe_migrate_resource_pay_meta();
|
||||
self::maybe_migrate_orders_to_repository();
|
||||
self::maybe_migrate_board_taxonomy_to_plate_cpt();
|
||||
}
|
||||
|
||||
private static function maybe_migrate_layout(): void {
|
||||
if (get_option(self::OPT_LAYOUT_MIGRATED)) return;
|
||||
|
||||
$settings = get_option('aksrt_settings', []);
|
||||
if (!is_array($settings)) $settings = [];
|
||||
|
||||
$basic = get_option('aksrt_basic_settings', []);
|
||||
if (!is_array($basic)) $basic = [];
|
||||
|
||||
$allowed = ['card', 'list', 'masonry', 'grid-cms'];
|
||||
|
||||
$existing_archive = $settings['archive_layout'] ?? '';
|
||||
if (!in_array($existing_archive, $allowed, true)) {
|
||||
// 优先取旧 default_archive_layout,其次旧 post_layout
|
||||
$candidate = $settings['default_archive_layout']
|
||||
?? $basic['post_layout']
|
||||
?? $settings['post_layout']
|
||||
?? 'card';
|
||||
if (!in_array($candidate, $allowed, true)) $candidate = 'card';
|
||||
$settings['archive_layout'] = $candidate;
|
||||
}
|
||||
|
||||
unset($settings['default_archive_layout']);
|
||||
unset($settings['post_layout']);
|
||||
update_option('aksrt_settings', $settings);
|
||||
|
||||
if (isset($basic['post_layout'])) {
|
||||
unset($basic['post_layout']);
|
||||
update_option('aksrt_basic_settings', $basic);
|
||||
}
|
||||
|
||||
update_option(self::OPT_LAYOUT_MIGRATED, time());
|
||||
}
|
||||
|
||||
/**
|
||||
* 把历史 aksrt_resource CPT 文章迁移到 post:
|
||||
* - post_type = aksrt_resource → post
|
||||
* - _aksrt_is_resource = 1
|
||||
*
|
||||
* 仅在 admin/cron 上下文且有数据时分批执行;幂等。
|
||||
*/
|
||||
private static function maybe_migrate_resource_cpt(): void {
|
||||
if (get_option(self::OPT_RESOURCE_MIGRATED)) return;
|
||||
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// 仅查 post_type 字段,避免拉全量行
|
||||
$ids = $wpdb->get_col("SELECT ID FROM {$wpdb->posts} WHERE post_type = 'aksrt_resource' LIMIT 500");
|
||||
if (empty($ids)) {
|
||||
// 无历史数据:直接打标记
|
||||
update_option(self::OPT_RESOURCE_MIGRATED, time());
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$id = (int) $id;
|
||||
// 转 post_type
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
['post_type' => 'post'],
|
||||
['ID' => $id],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
update_post_meta($id, '_aksrt_is_resource', 1);
|
||||
clean_post_cache($id);
|
||||
}
|
||||
|
||||
// 若一批迁移满 500,留待下次触发;否则收尾
|
||||
if (count($ids) < 500) {
|
||||
update_option(self::OPT_RESOURCE_MIGRATED, time());
|
||||
// 触发重写规则刷新(因为 /resource/{slug} 旧链接需要 301 重定向)
|
||||
delete_option('rewrite_rules');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v4 形态归一迁移:
|
||||
* - aksrt_settings 中的 site_profile / opening_mode / archive_layout
|
||||
* / default_archive_layout / post_layout 全部 unset
|
||||
* - aksrt_basic_settings 中的 opening_mode / post_layout 全部 unset
|
||||
* - 全部分类的 aksrt_flow term meta 删除
|
||||
*
|
||||
* 幂等。
|
||||
*/
|
||||
private static function maybe_migrate_cms_format(): void {
|
||||
if (get_option(self::OPT_CMS_FORMAT_MIGRATED)) return;
|
||||
|
||||
// 1. aksrt_settings
|
||||
$settings = get_option('aksrt_settings', []);
|
||||
if (is_array($settings)) {
|
||||
$changed = false;
|
||||
foreach (['site_profile', 'opening_mode', 'archive_layout', 'default_archive_layout', 'post_layout'] as $k) {
|
||||
if (array_key_exists($k, $settings)) {
|
||||
unset($settings[$k]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
update_option('aksrt_settings', $settings);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. aksrt_basic_settings
|
||||
$basic = get_option('aksrt_basic_settings', []);
|
||||
if (is_array($basic)) {
|
||||
$changed = false;
|
||||
foreach (['opening_mode', 'post_layout'] as $k) {
|
||||
if (array_key_exists($k, $basic)) {
|
||||
unset($basic[$k]);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
update_option('aksrt_basic_settings', $basic);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 删除全部分类的 aksrt_flow term meta
|
||||
global $wpdb;
|
||||
if ($wpdb instanceof wpdb) {
|
||||
$wpdb->delete($wpdb->termmeta, ['meta_key' => 'aksrt_flow'], ['%s']);
|
||||
}
|
||||
|
||||
update_option(self::OPT_CMS_FORMAT_MIGRATED, time());
|
||||
}
|
||||
|
||||
private static function maybe_migrate_resource_pay_meta(): void {
|
||||
if (get_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED)) return;
|
||||
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$batch_size = 200;
|
||||
$cursor = (int) get_option(self::OPT_RESOURCE_PAY_META_CURSOR, 0);
|
||||
$candidate_keys = [
|
||||
AKSRT_Domain_Model::PAY_META_KEY,
|
||||
AKSRT_Domain_Model::PAY_TYPE_KEY,
|
||||
AKSRT_Domain_Model::PAY_PRICE_KEY,
|
||||
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
|
||||
'_aksrt_res_pay_type',
|
||||
'_aksrt_res_price',
|
||||
'_aksrt_res_original_price',
|
||||
'_aksrt_res_vip1_price',
|
||||
'_aksrt_res_vip2_price',
|
||||
'_aksrt_res_version',
|
||||
'_aksrt_res_size',
|
||||
'_aksrt_res_license',
|
||||
'_aksrt_res_compat',
|
||||
'_aksrt_res_demo_url',
|
||||
'_aksrt_res_downloads',
|
||||
'_aksrt_res_require_login',
|
||||
'_aksrt_res_require_captcha',
|
||||
'download_count',
|
||||
];
|
||||
|
||||
$placeholders = implode(', ', array_fill(0, count($candidate_keys), '%s'));
|
||||
$sql = "SELECT DISTINCT pm.post_id
|
||||
FROM {$wpdb->postmeta} pm
|
||||
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
||||
WHERE p.post_type IN ('post', 'aksrt_resource')
|
||||
AND pm.meta_key IN ({$placeholders})
|
||||
AND pm.post_id > %d
|
||||
ORDER BY pm.post_id ASC
|
||||
LIMIT %d";
|
||||
|
||||
$params = array_merge($candidate_keys, [$cursor, $batch_size]);
|
||||
$ids = $wpdb->get_col($wpdb->prepare($sql, $params));
|
||||
|
||||
if (empty($ids)) {
|
||||
delete_option(self::OPT_RESOURCE_PAY_META_CURSOR);
|
||||
update_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED, time());
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($ids as $post_id) {
|
||||
self::migrate_resource_pay_meta_post((int) $post_id);
|
||||
}
|
||||
|
||||
update_option(self::OPT_RESOURCE_PAY_META_CURSOR, max(array_map('intval', $ids)));
|
||||
|
||||
if (count($ids) < $batch_size) {
|
||||
delete_option(self::OPT_RESOURCE_PAY_META_CURSOR);
|
||||
update_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED, time());
|
||||
}
|
||||
}
|
||||
|
||||
private static function maybe_migrate_orders_to_repository(): void {
|
||||
if (get_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION)) return;
|
||||
if (!class_exists('AKSRT_Order_Repository')) return;
|
||||
|
||||
AKSRT_Order_Repository::get_instance()->migrate_from_option();
|
||||
update_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION, time());
|
||||
}
|
||||
|
||||
/**
|
||||
* §7.0 板块 taxonomy → plate CPT 一次性迁移。
|
||||
*
|
||||
* 将 aksrt_forum_board taxonomy terms 转为 aksrt_forum_plate CPT posts,
|
||||
* 保留 term meta → post meta 映射、层级关系、版主数据。
|
||||
* 同时在每个帖子上写入 _aksrt_plate_id 指向新 plate post。
|
||||
* 旧 taxonomy 数据保留不删(向后兼容)。
|
||||
*/
|
||||
private static function maybe_migrate_board_taxonomy_to_plate_cpt(): void {
|
||||
if (get_option(self::OPT_PLATE_CPT_MIGRATED)) return;
|
||||
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
|
||||
if (!post_type_exists('aksrt_forum_plate')) return;
|
||||
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'aksrt_forum_board',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => '_aksrt_sort_order',
|
||||
'order' => 'ASC',
|
||||
]);
|
||||
|
||||
if (is_wp_error($terms) || empty($terms)) {
|
||||
update_option(self::OPT_PLATE_CPT_MIGRATED, time());
|
||||
return;
|
||||
}
|
||||
|
||||
$term_to_plate = [];
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$existing = get_posts([
|
||||
'post_type' => 'aksrt_forum_plate',
|
||||
'meta_key' => '_aksrt_migrated_from_term',
|
||||
'meta_value' => (string) $term->term_id,
|
||||
'numberposts' => 1,
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
]);
|
||||
|
||||
if (!empty($existing)) {
|
||||
$term_to_plate[$term->term_id] = (int) $existing[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
$plate_id = wp_insert_post([
|
||||
'post_type' => 'aksrt_forum_plate',
|
||||
'post_title' => $term->name,
|
||||
'post_content' => $term->description ?: '',
|
||||
'post_excerpt' => wp_trim_words($term->description ?: '', 30, ''),
|
||||
'post_status' => 'publish',
|
||||
'post_parent' => 0,
|
||||
'menu_order' => (int) get_term_meta($term->term_id, '_aksrt_sort_order', true),
|
||||
], true);
|
||||
|
||||
if (is_wp_error($plate_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$term_to_plate[$term->term_id] = $plate_id;
|
||||
|
||||
update_post_meta($plate_id, '_aksrt_migrated_from_term', $term->term_id);
|
||||
update_post_meta($plate_id, '_aksrt_sort_order', (int) get_term_meta($term->term_id, '_aksrt_sort_order', true));
|
||||
update_post_meta($plate_id, '_aksrt_icon', (string) get_term_meta($term->term_id, '_aksrt_icon', true));
|
||||
|
||||
$moderators = get_term_meta($term->term_id, '_aksrt_moderators', true);
|
||||
if (is_array($moderators) && !empty($moderators)) {
|
||||
update_post_meta($plate_id, '_aksrt_moderators', $moderators);
|
||||
}
|
||||
|
||||
$roles = get_term_meta($term->term_id, '_aksrt_moderator_roles', true);
|
||||
if (is_array($roles) && !empty($roles)) {
|
||||
update_post_meta($plate_id, '_aksrt_moderator_roles', $roles);
|
||||
}
|
||||
|
||||
$topic_count = (int) get_term_meta($term->term_id, '_aksrt_topic_count', true);
|
||||
if ($topic_count <= 0) {
|
||||
$topic_count = (int) $term->count;
|
||||
}
|
||||
update_post_meta($plate_id, '_aksrt_posts_count', $topic_count);
|
||||
}
|
||||
|
||||
// Resolve parent hierarchy
|
||||
foreach ($terms as $term) {
|
||||
if ($term->parent > 0 && isset($term_to_plate[$term->term_id], $term_to_plate[$term->parent])) {
|
||||
wp_update_post([
|
||||
'ID' => $term_to_plate[$term->term_id],
|
||||
'post_parent' => $term_to_plate[$term->parent],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign _aksrt_plate_id on existing topics
|
||||
global $wpdb;
|
||||
foreach ($term_to_plate as $term_id => $plate_id) {
|
||||
$topic_ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT tr.object_id FROM {$wpdb->term_relationships} tr
|
||||
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||
WHERE tt.term_id = %d AND tt.taxonomy = 'aksrt_forum_board'",
|
||||
$term_id
|
||||
));
|
||||
|
||||
foreach ($topic_ids as $topic_id) {
|
||||
update_post_meta((int) $topic_id, '_aksrt_plate_id', $plate_id);
|
||||
}
|
||||
}
|
||||
|
||||
delete_option('rewrite_rules');
|
||||
update_option(self::OPT_PLATE_CPT_MIGRATED, time());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Discount Engine
|
||||
*
|
||||
* 优惠活动计算引擎 — reduction 立减 / discount 打折 / gift 买赠。
|
||||
* scope: item / product / author / order。
|
||||
* 绑定在 shop_discount taxonomy 上,商品可挂多个活动。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Discount {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active discount campaigns for a product.
|
||||
*
|
||||
* @param int $product_id
|
||||
* @return array<int, array> Sorted by priority desc.
|
||||
*/
|
||||
public function get_active_discounts(int $product_id): array {
|
||||
$terms = wp_get_object_terms($product_id, 'shop_discount', ['fields' => 'all']);
|
||||
if (is_wp_error($terms) || empty($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$now = current_time('timestamp');
|
||||
$active = [];
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$meta = AKSRT_Shop_Term_Meta::get_discount_meta($term->term_id);
|
||||
|
||||
// Time limit check
|
||||
if (!empty($meta['start']) && strtotime($meta['start']) > $now) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($meta['end']) && strtotime($meta['end']) < $now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$meta['term_id'] = $term->term_id;
|
||||
$meta['name'] = $term->name;
|
||||
$active[] = $meta;
|
||||
}
|
||||
|
||||
// Sort by priority desc
|
||||
usort($active, function ($a, $b) {
|
||||
return ((int) $b['priority']) - ((int) $a['priority']);
|
||||
});
|
||||
|
||||
return $active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate discount for a single line item.
|
||||
*
|
||||
* @param float $unit_price Unit price
|
||||
* @param int $quantity Quantity
|
||||
* @param int $product_id Product ID
|
||||
* @param int $user_id User ID (for user_limit checks)
|
||||
* @return array{
|
||||
* original: float,
|
||||
* discount_amount: float,
|
||||
* final_price: float,
|
||||
* gifts: array,
|
||||
* applied: array<int, array>
|
||||
* }
|
||||
*/
|
||||
public function calculate(float $unit_price, int $quantity, int $product_id, int $user_id = 0): array {
|
||||
$original = $unit_price * $quantity;
|
||||
$discount_amount = 0.0;
|
||||
$gifts = [];
|
||||
$applied = [];
|
||||
|
||||
$discounts = $this->get_active_discounts($product_id);
|
||||
|
||||
foreach ($discounts as $d) {
|
||||
// User limit check
|
||||
if (!empty($d['user_limit']) && $user_id > 0) {
|
||||
if (!$this->check_user_limit($d['user_limit'], $user_id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Price limit check
|
||||
if ((float) $d['price_limit'] > 0 && $original < (float) $d['price_limit']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $d['type'] ?? 'reduction';
|
||||
$value = (float) ($d['value'] ?? 0);
|
||||
|
||||
switch ($type) {
|
||||
case 'reduction':
|
||||
// 立减
|
||||
$discount_amount += min($value * $quantity, $original - $discount_amount);
|
||||
break;
|
||||
|
||||
case 'discount':
|
||||
// 打折 (value = 折扣系数 e.g. 8.5 = 85折)
|
||||
if ($value > 0 && $value < 10) {
|
||||
$rate = $value / 10.0;
|
||||
$save = ($original - $discount_amount) * (1 - $rate);
|
||||
$discount_amount += $save;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'gift':
|
||||
// 买赠 — collect gift config, no price reduction
|
||||
if (!empty($d['gift_config'])) {
|
||||
$gift_data = json_decode($d['gift_config'], true);
|
||||
if (is_array($gift_data)) {
|
||||
$gifts[] = $gift_data;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$applied[] = [
|
||||
'term_id' => $d['term_id'],
|
||||
'name' => $d['name'] ?? '',
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
$discount_amount = min($discount_amount, $original);
|
||||
|
||||
return [
|
||||
'original' => $original,
|
||||
'discount_amount' => round($discount_amount, 2),
|
||||
'final_price' => round($original - $discount_amount, 2),
|
||||
'gifts' => $gifts,
|
||||
'applied' => $applied,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user meets the user_limit requirement.
|
||||
*
|
||||
* @param string $limit 'vip' / 'vip_2' / 'auth'
|
||||
* @param int $user_id
|
||||
* @return bool
|
||||
*/
|
||||
private function check_user_limit(string $limit, int $user_id): bool {
|
||||
if (empty($limit)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch ($limit) {
|
||||
case 'vip':
|
||||
return function_exists('aksrt_is_vip') ? aksrt_is_vip($user_id) : false;
|
||||
case 'vip_2':
|
||||
return function_exists('aksrt_is_vip2') ? aksrt_is_vip2($user_id) : false;
|
||||
case 'auth':
|
||||
return function_exists('aksrt_is_verified') ? aksrt_is_verified($user_id) : false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Docs Presenter
|
||||
*
|
||||
* Builds document tree structures, breadcrumb chains,
|
||||
* and chapter navigation for the docs mode.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Docs_Presenter {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document root page ID for a given page.
|
||||
*
|
||||
* @param int $page_id Current page ID.
|
||||
* @return int Root page ID (self if top-level, parent otherwise).
|
||||
*/
|
||||
public function get_root_id(int $page_id): int {
|
||||
$parent_id = wp_get_post_parent_id($page_id);
|
||||
return $parent_id ?: $page_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a flat ordered list of all pages under a document root.
|
||||
*
|
||||
* @param int $root_id Root page ID.
|
||||
* @param int $max_depth Maximum depth (default 4).
|
||||
* @return array Array of page objects with added '_depth' property.
|
||||
*/
|
||||
public function get_flat_tree(int $root_id, int $max_depth = 4): array {
|
||||
$result = [];
|
||||
$this->collect_children($root_id, $result, 0, $max_depth);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper to collect pages in order.
|
||||
*/
|
||||
private function collect_children(int $parent_id, array &$result, int $depth, int $max_depth): void {
|
||||
if ($depth >= $max_depth) {
|
||||
return;
|
||||
}
|
||||
|
||||
$children = get_pages([
|
||||
'parent' => $parent_id,
|
||||
'sort_column' => 'menu_order, post_title',
|
||||
'sort_order' => 'ASC',
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
|
||||
foreach ($children as $child) {
|
||||
$child->_depth = $depth;
|
||||
$result[] = $child;
|
||||
$this->collect_children($child->ID, $result, $depth + 1, $max_depth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sibling pages for chapter navigation (prev/next).
|
||||
*
|
||||
* @param int $page_id Current page ID.
|
||||
* @return array{prev: object|null, next: object|null}
|
||||
*/
|
||||
public function get_chapter_nav(int $page_id): array {
|
||||
$parent_id = wp_get_post_parent_id($page_id);
|
||||
|
||||
$siblings = get_pages([
|
||||
'parent' => $parent_id ?: 0,
|
||||
'sort_column' => 'menu_order, post_title',
|
||||
'sort_order' => 'ASC',
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
|
||||
$current_index = -1;
|
||||
foreach ($siblings as $i => $sibling) {
|
||||
if ((int) $sibling->ID === $page_id) {
|
||||
$current_index = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$prev = $current_index > 0 ? $siblings[$current_index - 1] : null;
|
||||
$next = ($current_index >= 0 && $current_index < count($siblings) - 1)
|
||||
? $siblings[$current_index + 1]
|
||||
: null;
|
||||
|
||||
return ['prev' => $prev, 'next' => $next];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get root-level document pages for the document navigation index.
|
||||
*
|
||||
* @param string $order Sort order: 'menu_order', 'date', 'title'.
|
||||
* @return array
|
||||
*/
|
||||
public function get_root_pages(string $order = 'menu_order'): array {
|
||||
$sort_column = 'menu_order, post_title';
|
||||
if ($order === 'title') {
|
||||
$sort_column = 'post_title';
|
||||
} elseif ($order === 'date') {
|
||||
$sort_column = 'post_date';
|
||||
}
|
||||
|
||||
return get_pages([
|
||||
'sort_column' => $sort_column,
|
||||
'sort_order' => 'ASC',
|
||||
'parent' => 0,
|
||||
'hierarchical' => 0,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recently modified pages for the sidebar.
|
||||
*
|
||||
* @param int $count Number of pages to return.
|
||||
* @return array
|
||||
*/
|
||||
public function get_recent_pages(int $count = 8): array {
|
||||
return get_pages([
|
||||
'sort_column' => 'post_modified',
|
||||
'sort_order' => 'DESC',
|
||||
'number' => $count,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build breadcrumb trail for a document page.
|
||||
*
|
||||
* @param int $page_id Current page ID.
|
||||
* @return array Array of {id, title, url} objects.
|
||||
*/
|
||||
public function get_breadcrumbs(int $page_id): array {
|
||||
$trail = [];
|
||||
$current = get_post($page_id);
|
||||
|
||||
while ($current) {
|
||||
array_unshift($trail, [
|
||||
'id' => $current->ID,
|
||||
'title' => $current->post_title,
|
||||
'url' => get_permalink($current->ID),
|
||||
]);
|
||||
$parent_id = wp_get_post_parent_id($current->ID);
|
||||
$current = $parent_id ? get_post($parent_id) : null;
|
||||
}
|
||||
|
||||
return $trail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search document pages by keyword.
|
||||
*
|
||||
* @param string $keyword Search keyword.
|
||||
* @param int $root_id Optional root page ID to scope search.
|
||||
* @param int $limit Max results.
|
||||
* @return array Array of page objects with 'url' and 'excerpt' added.
|
||||
*/
|
||||
public function search_pages(string $keyword, int $root_id = 0, int $limit = 20): array {
|
||||
$args = [
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
's' => $keyword,
|
||||
'posts_per_page' => $limit,
|
||||
'orderby' => 'relevance',
|
||||
];
|
||||
|
||||
if ($root_id > 0) {
|
||||
$tree_ids = [$root_id];
|
||||
$this->collect_children($root_id, $tree_ids, 0, 4);
|
||||
$ids = array_map(static function ($item) {
|
||||
return is_object($item) ? $item->ID : (int) $item;
|
||||
}, $tree_ids);
|
||||
$ids[] = $root_id;
|
||||
$args['post__in'] = array_unique($ids);
|
||||
}
|
||||
|
||||
$query = new WP_Query($args);
|
||||
$results = [];
|
||||
|
||||
foreach ($query->posts as $post) {
|
||||
$results[] = [
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'url' => get_permalink($post->ID),
|
||||
'excerpt' => wp_trim_words(wp_strip_all_tags($post->post_content), 30),
|
||||
'modified' => $post->post_modified,
|
||||
'parent' => (int) $post->post_parent,
|
||||
];
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given page belongs to the docs mode.
|
||||
*
|
||||
* @param int $page_id Page ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_docs_page(int $page_id): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$settings = AKSRT_Settings::get_docs_mode_settings();
|
||||
if (empty($settings['enabled'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$template = get_page_template_slug($page_id);
|
||||
return in_array($template, [
|
||||
'templates/pages/page-documentnav.php',
|
||||
'templates/pages/page-special.php',
|
||||
], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Download State
|
||||
*
|
||||
* Unified download state resolver for resource pages.
|
||||
* Replaces scattered per-template download status checks
|
||||
* with a single service that returns a normalized state object.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Download_State {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* State constants
|
||||
*/
|
||||
const STATE_NOT_LOGGED_IN = 'not_logged_in';
|
||||
const STATE_NO_PERMISSION = 'no_permission';
|
||||
const STATE_INSUFFICIENT_CREDITS = 'insufficient_credits';
|
||||
const STATE_OWNED = 'owned';
|
||||
const STATE_AVAILABLE = 'available';
|
||||
const STATE_PENDING_DELIVERY = 'pending_delivery';
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current user's download state for a given resource post.
|
||||
*
|
||||
* @param int $post_id Resource post ID.
|
||||
* @return array {
|
||||
* @type string $state One of the STATE_* constants.
|
||||
* @type bool $can_download Whether the user can initiate download.
|
||||
* @type string $message Human-readable status message.
|
||||
* @type string $action_label Suggested button label.
|
||||
* @type string $action_url Suggested button URL (if applicable).
|
||||
* @type int $cost Download cost in credits.
|
||||
* @type int $balance Current user credit balance.
|
||||
* @type string $login_url Login URL (if not logged in).
|
||||
* }
|
||||
*/
|
||||
public function resolve(int $post_id): array {
|
||||
$meta = function_exists('aksrt_get_resource_meta') ? aksrt_get_resource_meta($post_id) : [];
|
||||
$price = (float) ($meta['price'] ?? 0);
|
||||
$downloads = is_array($meta['downloads'] ?? null) ? $meta['downloads'] : [];
|
||||
|
||||
$is_logged_in = is_user_logged_in();
|
||||
$user_id = $is_logged_in ? get_current_user_id() : 0;
|
||||
$login_url = wp_login_url(get_permalink($post_id));
|
||||
|
||||
// No downloads available
|
||||
if (empty($downloads)) {
|
||||
return $this->build_result(
|
||||
self::STATE_NO_PERMISSION,
|
||||
false,
|
||||
__('该资源暂未提供下载链接。', 'aksrt'),
|
||||
__('暂无下载', 'aksrt'),
|
||||
'',
|
||||
0, 0, $login_url
|
||||
);
|
||||
}
|
||||
|
||||
// Not logged in
|
||||
if (!$is_logged_in) {
|
||||
$requires_login = false;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$paid = AKSRT_Settings::get_paid_content_settings();
|
||||
$requires_login = !empty($paid['download_requires_login']);
|
||||
}
|
||||
if ($requires_login) {
|
||||
return $this->build_result(
|
||||
self::STATE_NOT_LOGGED_IN,
|
||||
false,
|
||||
__('登录后可下载此资源。', 'aksrt'),
|
||||
__('登录下载', 'aksrt'),
|
||||
$login_url,
|
||||
0, 0, $login_url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check gate (credits, permission, etc.)
|
||||
$gate = function_exists('aksrt_can_download') ? aksrt_can_download($post_id) : true;
|
||||
if (is_wp_error($gate)) {
|
||||
$code = $gate->get_error_code();
|
||||
$data = (array) $gate->get_error_data();
|
||||
$balance = (int) ($data['balance'] ?? 0);
|
||||
$cost = function_exists('aksrt_resource_download_cost')
|
||||
? aksrt_resource_download_cost($post_id)
|
||||
: (int) ceil($price);
|
||||
|
||||
if ($code === 'aksrt_need_login') {
|
||||
return $this->build_result(
|
||||
self::STATE_NOT_LOGGED_IN,
|
||||
false,
|
||||
__('登录后可下载此资源。', 'aksrt'),
|
||||
__('登录下载', 'aksrt'),
|
||||
(string) ($data['login_url'] ?? $login_url),
|
||||
$cost, $balance, (string) ($data['login_url'] ?? $login_url)
|
||||
);
|
||||
}
|
||||
|
||||
if ($code === 'aksrt_credits_insufficient') {
|
||||
return $this->build_result(
|
||||
self::STATE_INSUFFICIENT_CREDITS,
|
||||
false,
|
||||
sprintf(
|
||||
/* translators: 1: current balance, 2: required cost */
|
||||
__('当前余额 %1$d 积分,需要 %2$d 积分。', 'aksrt'),
|
||||
$balance,
|
||||
$cost
|
||||
),
|
||||
sprintf(__('付费下载(%d 积分)', 'aksrt'), $cost),
|
||||
'',
|
||||
$cost, $balance, $login_url
|
||||
);
|
||||
}
|
||||
|
||||
// Other gate error — treat as no permission
|
||||
return $this->build_result(
|
||||
self::STATE_NO_PERMISSION,
|
||||
false,
|
||||
$gate->get_error_message() ?: __('无下载权限。', 'aksrt'),
|
||||
__('无权限', 'aksrt'),
|
||||
'',
|
||||
0, 0, $login_url
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already owns/downloads
|
||||
$has_downloaded = false;
|
||||
if ($is_logged_in) {
|
||||
$downloaded = get_user_meta($user_id, '_aksrt_downloaded_resources', true);
|
||||
$downloaded = is_array($downloaded) ? $downloaded : [];
|
||||
$has_downloaded = in_array($post_id, $downloaded, true);
|
||||
}
|
||||
|
||||
$cost = function_exists('aksrt_resource_download_cost')
|
||||
? aksrt_resource_download_cost($post_id)
|
||||
: (int) ceil($price);
|
||||
$balance = 0;
|
||||
if ($is_logged_in && class_exists('AKSRT_Credits')) {
|
||||
$balance = AKSRT_Credits::balance($user_id);
|
||||
}
|
||||
|
||||
if ($has_downloaded) {
|
||||
return $this->build_result(
|
||||
self::STATE_OWNED,
|
||||
true,
|
||||
__('已下载,可再次下载。', 'aksrt'),
|
||||
__('再次下载', 'aksrt'),
|
||||
'',
|
||||
$cost, $balance, $login_url
|
||||
);
|
||||
}
|
||||
|
||||
// Available
|
||||
$label = $cost > 0
|
||||
? sprintf(__('下载(%d 积分)', 'aksrt'), $cost)
|
||||
: __('立即下载', 'aksrt');
|
||||
|
||||
return $this->build_result(
|
||||
self::STATE_AVAILABLE,
|
||||
true,
|
||||
$cost > 0
|
||||
? sprintf(__('下载此资源将消耗 %d 积分。', 'aksrt'), $cost)
|
||||
: __('此资源可免费下载。', 'aksrt'),
|
||||
$label,
|
||||
'',
|
||||
$cost, $balance, $login_url
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a normalized result array.
|
||||
*/
|
||||
private function build_result(
|
||||
string $state,
|
||||
bool $can_download,
|
||||
string $message,
|
||||
string $action_label,
|
||||
string $action_url,
|
||||
int $cost,
|
||||
int $balance,
|
||||
string $login_url
|
||||
): array {
|
||||
return [
|
||||
'state' => $state,
|
||||
'can_download' => $can_download,
|
||||
'message' => $message,
|
||||
'action_label' => $action_label,
|
||||
'action_url' => $action_url,
|
||||
'cost' => $cost,
|
||||
'balance' => $balance,
|
||||
'login_url' => $login_url,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Download
|
||||
*
|
||||
* Resource download flow:
|
||||
* 1) Frontend calls POST /aksrt/v1/download/request to get a short-lived token link.
|
||||
* 2) User visits /aksrt-dl/{token}, token is validated, consumed, then redirected.
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Download {
|
||||
|
||||
const TOKEN_TTL = 600; // 10 minutes
|
||||
const TOKEN_LEN = 48;
|
||||
const QUERY_VAR = 'aksrt_dl_token';
|
||||
const ENDPOINT = 'aksrt-dl';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('init', [$this, 'register_rewrite']);
|
||||
add_filter('query_vars', [$this, 'add_query_var']);
|
||||
add_action('template_redirect', [$this, 'maybe_serve_download'], 0);
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
|
||||
add_action('wp', [$this, 'maybe_schedule_cleanup']);
|
||||
add_action('aksrt_dl_token_cleanup', [$this, 'cleanup_expired_tokens']);
|
||||
}
|
||||
|
||||
public function maybe_schedule_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_dl_token_cleanup')) {
|
||||
$next = (new DateTimeImmutable('tomorrow 03:30', wp_timezone()))->getTimestamp();
|
||||
wp_schedule_event($next, 'daily', 'aksrt_dl_token_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
public function cleanup_expired_tokens(): void {
|
||||
global $wpdb;
|
||||
if (!class_exists('AKSRT_DB_Migrator')) return;
|
||||
|
||||
$table = AKSRT_DB_Migrator::table('dl_token');
|
||||
if (!$table) return;
|
||||
|
||||
$now_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$audit_cutoff = (new DateTimeImmutable('-7 days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE expires_at < %s",
|
||||
$now_utc
|
||||
));
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE used_count >= max_uses AND created_at < %s",
|
||||
$audit_cutoff
|
||||
));
|
||||
}
|
||||
|
||||
public function register_rewrite(): void {
|
||||
add_rewrite_rule(
|
||||
'^' . self::ENDPOINT . '/([A-Za-z0-9]{1,64})/?$',
|
||||
'index.php?' . self::QUERY_VAR . '=$matches[1]',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
public function add_query_var(array $vars): array {
|
||||
$vars[] = self::QUERY_VAR;
|
||||
return $vars;
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/download/request', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_request'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/download/policy/(?P<id>\\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_policy'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
}
|
||||
|
||||
public function check_rest_nonce(WP_REST_Request $request) {
|
||||
$nonce = $request->get_header('x-wp-nonce');
|
||||
if (!$nonce) {
|
||||
return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if (!wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rest_policy(WP_REST_Request $request) {
|
||||
$post_id = (int) $request['id'];
|
||||
$post = get_post($post_id);
|
||||
if (!$post || !aksrt_is_resource_post($post)) {
|
||||
return new WP_Error('aksrt_invalid', __('资源不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$meta = aksrt_get_resource_meta($post);
|
||||
$paid_settings = AKSRT_Settings::get_paid_content_settings();
|
||||
$cost = aksrt_resource_download_cost($post);
|
||||
$uid = get_current_user_id();
|
||||
|
||||
$need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
|
||||
if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
|
||||
$need_captcha = false;
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'post_id' => $post_id,
|
||||
'price' => (float) $meta['price'],
|
||||
'cost' => $cost,
|
||||
'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']),
|
||||
'require_captcha' => $need_captcha,
|
||||
'logged_in' => $uid > 0,
|
||||
'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0,
|
||||
'sources' => array_map(function ($row, $idx) {
|
||||
$extract_code = trim((string) ($row['extract_code'] ?? ''));
|
||||
return [
|
||||
'index' => $idx,
|
||||
'label' => $row['label'] ?? '',
|
||||
'type' => $row['type'] ?? 'direct',
|
||||
'has_extract' => $extract_code !== '',
|
||||
];
|
||||
}, $meta['downloads'], array_keys($meta['downloads'])),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function rest_request(WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) $params = $request->get_params();
|
||||
|
||||
$post_id = isset($params['post_id']) ? (int) $params['post_id'] : 0;
|
||||
$source_index = isset($params['source_index']) ? (int) $params['source_index'] : 0;
|
||||
|
||||
$post = get_post($post_id);
|
||||
if (!$post || !aksrt_is_resource_post($post)) {
|
||||
return new WP_Error('aksrt_invalid', __('资源不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$meta = aksrt_get_resource_meta($post);
|
||||
if (empty($meta['downloads'][$source_index])) {
|
||||
return new WP_Error('aksrt_no_source', __('下载源不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$source = $meta['downloads'][$source_index];
|
||||
|
||||
$gate = aksrt_can_download($post);
|
||||
if (is_wp_error($gate)) return $gate;
|
||||
|
||||
$paid_settings = AKSRT_Settings::get_paid_content_settings();
|
||||
$need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
|
||||
if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
|
||||
$need_captcha = false;
|
||||
}
|
||||
if ($need_captcha && function_exists('aksrt_verify_captcha')) {
|
||||
$cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : [];
|
||||
$verify = aksrt_verify_captcha($cap_payload);
|
||||
if ($verify !== true) {
|
||||
$msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt');
|
||||
return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]);
|
||||
}
|
||||
}
|
||||
|
||||
$uid = get_current_user_id();
|
||||
$cost = aksrt_resource_download_cost($post);
|
||||
if ($cost > 0 && $uid > 0 && class_exists('AKSRT_Credits')) {
|
||||
$deducted = AKSRT_Credits::deduct($uid, $cost, 'download', ['type' => 'post', 'id' => $post_id]);
|
||||
if (is_wp_error($deducted)) return $deducted;
|
||||
}
|
||||
|
||||
$token = $this->issue_token($uid, $post_id, $source_index, $cost);
|
||||
if (is_wp_error($token)) {
|
||||
if ($cost > 0 && $uid > 0 && class_exists('AKSRT_Credits')) {
|
||||
AKSRT_Credits::add($uid, $cost, 'refund', ['type' => 'post', 'id' => $post_id]);
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
$dl_url = home_url('/' . self::ENDPOINT . '/' . $token);
|
||||
if ($uid > 0 && function_exists('aksrt_add_user_notification')) {
|
||||
aksrt_add_user_notification($uid, [
|
||||
'type' => 'download_ready',
|
||||
'title' => __('资源下载已就绪', 'aksrt'),
|
||||
'desc' => sprintf(__('《%s》下载链接已生成,可立即开始下载。', 'aksrt'), get_the_title($post)),
|
||||
'url' => $dl_url,
|
||||
]);
|
||||
}
|
||||
|
||||
$extract_code = trim((string) ($source['extract_code'] ?? ''));
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'token' => $token,
|
||||
'dl_url' => $dl_url,
|
||||
'expires_in' => self::TOKEN_TTL,
|
||||
'source_label' => $source['label'] ?? '',
|
||||
'source_type' => $source['type'] ?? 'direct',
|
||||
'has_extract' => $extract_code !== '',
|
||||
'extract_code' => $extract_code,
|
||||
'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function maybe_serve_download(): void {
|
||||
$token = get_query_var(self::QUERY_VAR);
|
||||
if (!$token) return;
|
||||
|
||||
$token = (string) $token;
|
||||
if (strlen($token) > 64 || !preg_match('/^[A-Za-z0-9]+$/', $token)) {
|
||||
$this->fail_410();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('dl_token');
|
||||
if (!$table) $this->fail_410();
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE token = %s LIMIT 1",
|
||||
$token
|
||||
), ARRAY_A);
|
||||
if (!$row) $this->fail_410();
|
||||
|
||||
$expires_at = strtotime($row['expires_at'] . ' UTC');
|
||||
if ($expires_at !== false && time() > $expires_at) $this->fail_410();
|
||||
|
||||
if ((int) $row['used_count'] >= (int) $row['max_uses']) $this->fail_410();
|
||||
|
||||
$ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$ua_hash = sha1((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||||
if (!empty($row['ip_bind']) && $row['ip_bind'] !== $ip) $this->fail_410();
|
||||
if (!empty($row['ua_hash']) && $row['ua_hash'] !== $ua_hash) $this->fail_410();
|
||||
|
||||
$post = get_post((int) $row['post_id']);
|
||||
if (!$post || !aksrt_is_resource_post($post)) $this->fail_410();
|
||||
$meta = aksrt_get_resource_meta($post);
|
||||
$idx = (int) $row['source_index'];
|
||||
if (empty($meta['downloads'][$idx]['url'])) $this->fail_410();
|
||||
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['used_count' => (int) $row['used_count'] + 1],
|
||||
['token' => $token],
|
||||
['%d'],
|
||||
['%s']
|
||||
);
|
||||
|
||||
// Count on actual token consumption.
|
||||
if (function_exists('aksrt_update_post_counter')) {
|
||||
aksrt_update_post_counter((int) $row['post_id'], '_aksrt_res_download_count', 1);
|
||||
} else {
|
||||
$count = (int) get_post_meta((int) $row['post_id'], '_aksrt_res_download_count', true);
|
||||
update_post_meta((int) $row['post_id'], '_aksrt_res_download_count', $count + 1);
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
wp_redirect(esc_url_raw((string) $meta['downloads'][$idx]['url']), 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function fail_410(): void {
|
||||
status_header(410);
|
||||
nocache_headers();
|
||||
wp_die(esc_html__('下载链接已过期或无效,请回到资源页重新获取。', 'aksrt'), 410);
|
||||
}
|
||||
|
||||
private function issue_token(int $user_id, int $post_id, int $source_index, int $cost) {
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('dl_token');
|
||||
if (!$table) {
|
||||
return new WP_Error('aksrt_no_table', __('下载令牌表未初始化', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$token = $this->random_token(self::TOKEN_LEN);
|
||||
|
||||
$ip = function_exists('aksrt_get_client_ip') ? (string) aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$ua_hash = sha1((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
|
||||
|
||||
$now_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$expires_utc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->modify('+' . self::TOKEN_TTL . ' seconds')->format('Y-m-d H:i:s');
|
||||
|
||||
$ok = $wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'token' => $token,
|
||||
'user_id' => $user_id,
|
||||
'post_id' => $post_id,
|
||||
'source_index' => $source_index,
|
||||
'cost' => $cost,
|
||||
'ip_bind' => $ip,
|
||||
'ua_hash' => $ua_hash,
|
||||
'used_count' => 0,
|
||||
'max_uses' => 1,
|
||||
'expires_at' => $expires_utc,
|
||||
'created_at' => $now_utc,
|
||||
],
|
||||
['%s', '%d', '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($ok === false) {
|
||||
return new WP_Error('aksrt_token_insert', __('生成下载令牌失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function random_token(int $len): string {
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
$max = strlen($alphabet) - 1;
|
||||
$out = '';
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$out .= $alphabet[random_int(0, $max)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Email Rollback
|
||||
*
|
||||
* 用户修改邮箱后会写入两条 user_meta:
|
||||
* - aksrt_email_previous 旧邮箱
|
||||
* - aksrt_email_changed_at 时间戳
|
||||
*
|
||||
* 14 天内管理员可执行回滚:把账号邮箱改回旧值并清除回滚 meta。
|
||||
*
|
||||
* 入口:
|
||||
* - 后台用户列表新增"邮箱回滚"行操作
|
||||
* - REST: GET /aksrt/v1/email-rollback/users 列出窗口内可回滚的用户
|
||||
* - REST: POST /aksrt/v1/email-rollback/users/{id}/rollback 执行回滚
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Email_Rollback {
|
||||
|
||||
const META_PREVIOUS = 'aksrt_email_previous';
|
||||
const META_CHANGED_AT = 'aksrt_email_changed_at';
|
||||
const WINDOW_SECONDS = 1209600; // 14 days
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('user_row_actions', [$this, 'add_row_action'], 20, 2);
|
||||
add_action('admin_post_aksrt_email_rollback', [$this, 'handle_admin_rollback']);
|
||||
// cron 清理过期 meta
|
||||
add_action('aksrt_email_rollback_cleanup', [self::class, 'cron_cleanup']);
|
||||
add_action('wp', [self::class, 'maybe_schedule_cleanup']);
|
||||
}
|
||||
|
||||
public static function maybe_schedule_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_email_rollback_cleanup')) {
|
||||
wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'aksrt_email_rollback_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
public static function cron_cleanup(): void {
|
||||
global $wpdb;
|
||||
$cutoff = time() - self::WINDOW_SECONDS;
|
||||
$rows = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) < %d",
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff
|
||||
));
|
||||
if (empty($rows)) return;
|
||||
foreach ($rows as $uid) {
|
||||
delete_user_meta((int) $uid, self::META_PREVIOUS);
|
||||
delete_user_meta((int) $uid, self::META_CHANGED_AT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 14 天窗口内?
|
||||
*/
|
||||
public static function is_within_window(int $user_id): bool {
|
||||
$changed_at = (int) get_user_meta($user_id, self::META_CHANGED_AT, true);
|
||||
if ($changed_at <= 0) return false;
|
||||
return (time() - $changed_at) <= self::WINDOW_SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出窗口内可回滚的用户。
|
||||
*/
|
||||
public static function list_pending(int $page = 1, int $per_page = 20): array {
|
||||
global $wpdb;
|
||||
$page = max(1, $page);
|
||||
$per_page = min(100, max(1, $per_page));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$cutoff = time() - self::WINDOW_SECONDS;
|
||||
|
||||
$sql_count = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT um1.user_id)
|
||||
FROM {$wpdb->usermeta} um1
|
||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d",
|
||||
self::META_PREVIOUS,
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff
|
||||
);
|
||||
$total = (int) $wpdb->get_var($sql_count);
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT um1.user_id, um1.meta_value AS changed_at, um2.meta_value AS previous_email
|
||||
FROM {$wpdb->usermeta} um1
|
||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d
|
||||
ORDER BY CAST(um1.meta_value AS UNSIGNED) DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
self::META_PREVIOUS,
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff,
|
||||
$per_page,
|
||||
$offset
|
||||
);
|
||||
$rows = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $r) {
|
||||
$uid = (int) $r['user_id'];
|
||||
$u = get_userdata($uid);
|
||||
if (!$u) continue;
|
||||
$items[] = [
|
||||
'user_id' => $uid,
|
||||
'login' => $u->user_login,
|
||||
'display_name' => $u->display_name,
|
||||
'current_email' => $u->user_email,
|
||||
'previous_email' => (string) $r['previous_email'],
|
||||
'changed_at' => (int) $r['changed_at'],
|
||||
'expires_in' => max(0, (int) $r['changed_at'] + self::WINDOW_SECONDS - time()),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行回滚。
|
||||
*
|
||||
* @return array{success:bool, message?:string, error?:string}
|
||||
*/
|
||||
public static function rollback(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
if (!self::is_within_window($user_id)) {
|
||||
return ['success' => false, 'error' => __('回滚窗口已过期或当前账号未发起过邮箱变更', 'aksrt')];
|
||||
}
|
||||
|
||||
$previous = (string) get_user_meta($user_id, self::META_PREVIOUS, true);
|
||||
if ($previous === '' || !is_email($previous)) {
|
||||
return ['success' => false, 'error' => __('旧邮箱无效', 'aksrt')];
|
||||
}
|
||||
|
||||
// 检查旧邮箱是否被其他用户占用(理论上极少见)
|
||||
$occupier = email_exists($previous);
|
||||
if ($occupier && (int) $occupier !== $user_id) {
|
||||
return ['success' => false, 'error' => __('旧邮箱已被其他账号占用,无法回滚', 'aksrt')];
|
||||
}
|
||||
|
||||
$update = wp_update_user([
|
||||
'ID' => $user_id,
|
||||
'user_email' => $previous,
|
||||
]);
|
||||
if (is_wp_error($update)) {
|
||||
return ['success' => false, 'error' => $update->get_error_message()];
|
||||
}
|
||||
|
||||
// 清空回滚 meta(一次性)
|
||||
delete_user_meta($user_id, self::META_PREVIOUS);
|
||||
delete_user_meta($user_id, self::META_CHANGED_AT);
|
||||
|
||||
// 记录管理员操作日志
|
||||
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
|
||||
if (!is_array($log)) $log = [];
|
||||
$log[] = [
|
||||
't' => time(),
|
||||
'action' => 'email_rollback',
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
];
|
||||
if (count($log) > 10) $log = array_slice($log, -10);
|
||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
||||
|
||||
if (class_exists('AKSRT_Auth_Service')) {
|
||||
AKSRT_Auth_Service::log_event('admin.email_rollback', [
|
||||
'uid' => $user_id,
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在用户列表行操作中加入"回滚邮箱"。
|
||||
*/
|
||||
public function add_row_action(array $actions, WP_User $user): array {
|
||||
if (!current_user_can('edit_user', $user->ID)) return $actions;
|
||||
if (!self::is_within_window((int) $user->ID)) return $actions;
|
||||
|
||||
$url = wp_nonce_url(
|
||||
admin_url('admin-post.php?action=aksrt_email_rollback&user_id=' . (int) $user->ID),
|
||||
'aksrt_email_rollback_' . (int) $user->ID
|
||||
);
|
||||
$actions['aksrt_email_rollback'] = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url($url),
|
||||
esc_html__('回滚邮箱', 'aksrt')
|
||||
);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function handle_admin_rollback(): void {
|
||||
$user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
|
||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_email_rollback_' . $user_id);
|
||||
|
||||
$r = self::rollback($user_id, get_current_user_id());
|
||||
$type = !empty($r['success']) ? 'success' : 'error';
|
||||
$msg = $r['message'] ?? $r['error'] ?? '';
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [['type' => $type, 'msg' => $msg]], 30);
|
||||
|
||||
wp_safe_redirect(wp_get_referer() ?: admin_url('users.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Follow — 关注系统
|
||||
*
|
||||
* 使用专用表 {prefix}aksrt_follow 存储关注关系。
|
||||
* 支持:关注、取消关注、计数、是否已关注。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Follow {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return AKSRT_DB_Migrator::table('follow');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
*
|
||||
* @param int $follower_id 关注者 ID
|
||||
* @param int $following_id 被关注者 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function follow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0 || $follower_id === $following_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已关注则跳过
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%d', '%s']);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消关注
|
||||
*/
|
||||
public function unfollow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->delete($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
], ['%d', '%d']);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换关注状态
|
||||
*
|
||||
* @return bool true=已关注, false=已取消
|
||||
*/
|
||||
public function toggle(int $follower_id, int $following_id): bool {
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
$this->unfollow($follower_id, $following_id);
|
||||
return false;
|
||||
}
|
||||
$this->follow($follower_id, $following_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已关注
|
||||
*/
|
||||
public function is_following(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE follower_id = %d AND following_id = %d LIMIT 1",
|
||||
$follower_id, $following_id
|
||||
));
|
||||
|
||||
return (int) $exists > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝数(谁关注了 target_id)
|
||||
*/
|
||||
public function get_follower_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE following_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注数(target_id 关注了谁)
|
||||
*/
|
||||
public function get_following_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE follower_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝列表
|
||||
*/
|
||||
public function get_followers(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'following_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注列表
|
||||
*/
|
||||
public function get_following(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'follower_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用用户列表查询
|
||||
*/
|
||||
private function get_user_list(int $user_id, string $where_field, int $page, int $per_page): array {
|
||||
global $wpdb;
|
||||
|
||||
$other_field = $where_field === 'follower_id' ? 'following_id' : 'follower_id';
|
||||
$offset = max(0, ($page - 1) * $per_page);
|
||||
|
||||
$total = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE {$where_field} = %d",
|
||||
$user_id
|
||||
));
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT {$other_field} as user_id, created_at
|
||||
FROM {$this->table()}
|
||||
WHERE {$where_field} = %d
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
$user_id, $per_page, $offset
|
||||
), ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$uid = (int) $row['user_id'];
|
||||
$user = get_userdata($uid);
|
||||
$items[] = [
|
||||
'user_id' => $uid,
|
||||
'display_name' => $user ? $user->display_name : '',
|
||||
'avatar_url' => $user ? get_avatar_url($uid, ['size' => 48]) : '',
|
||||
'created_at' => $row['created_at'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
];
|
||||
}
|
||||
}
|
||||
Loaded 100 of 595 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user