导入 AKSRT 主题开发源码

This commit is contained in:
Claude Code committed 2026-07-03 22:50:34 +08:00
1 parent 9681dd3d48
commit 1268a55dd0
595 files changed
+127705

No files matched your search

+420
View File
@@ -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')]);
}
}
@@ -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;
}
}
@@ -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]
);
}
}
+254
View File
@@ -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,
]);
}
}
+800
View File
@@ -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'),
];
}
}
+117
View File
@@ -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;
}
}
+111
View File
@@ -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;
}
}
+185
View File
@@ -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);
}
}
+250
View File
@@ -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)
);
}
}
+217
View File
@@ -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;
}
}
+414
View File
@@ -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');
}
}
+209
View File
@@ -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),
];
}
}
+348
View File
@@ -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,
],
]);
}
}
+357
View File
@@ -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')]);
}
}
+276
View File
@@ -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;
}
}
+229
View File
@@ -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.
}
}
+308
View File
@@ -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);
}
}
+386
View File
@@ -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());
}
}
+171
View File
@@ -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;
}
}
}
+232
View File
@@ -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);
}
}
+208
View File
@@ -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,
];
}
}
+329
View File
@@ -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;
}
}
+228
View File
@@ -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;
}
}
+179
View File
@@ -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