From 2bbd7c01a3998fc6628c6bdb6cf0827f26351e34 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Aug 2026 20:02:44 +0800 Subject: [PATCH 1/2] feat: remove forum feature (Phase 1) --- assets/admin/src/api/index.js | 3 +- assets/admin/src/constants/sections/forum.js | 111 -- assets/admin/src/constants/sections/index.js | 6 - .../pages/content/FloatingButtonsSettings.vue | 4 +- .../pages/content/FooterTabbarSettings.vue | 12 +- assets/admin/src/pages/forum/ForumBoards.vue | 195 -- .../admin/src/pages/forum/ForumDashboard.vue | 223 --- .../forum/ForumModeratorApplications.vue | 201 -- .../admin/src/pages/forum/ForumModerators.vue | 363 ---- assets/admin/src/pages/forum/ForumTopics.vue | 186 -- .../components/TopicModerationDrawer.vue | 284 --- .../src/pages/payment/PaymentGateway.vue | 6 +- assets/admin/src/pages/tools/ToolsUpdate.vue | 1 - .../src/pages/users/UsersWechatTemplate.vue | 1 - .../src/router/component-loaders/forum.js | 2 - .../src/router/component-loaders/index.js | 4 - assets/admin/src/router/index.js | 12 - assets/admin/src/schema/index.js | 6 - .../src/schema/sections/extensions/audit.js | 1 - .../admin/src/schema/sections/forum/basic.js | 28 - .../src/schema/sections/forum/board-page.js | 31 - .../src/schema/sections/forum/credits.js | 37 - .../src/schema/sections/forum/homepage.js | 29 - .../admin/src/schema/sections/forum/index.js | 31 - .../admin/src/schema/sections/forum/names.js | 41 - .../admin/src/schema/sections/forum/other.js | 64 - .../src/schema/sections/forum/permissions.js | 64 - .../src/schema/sections/forum/post-page.js | 60 - .../admin/src/schema/sections/forum/reply.js | 44 - .../admin/src/schema/sections/forum/upload.js | 27 - .../src/schema/sections/global/search.js | 6 - .../src/schema/sections/page-display/index.js | 16 +- .../admin/src/schema/sections/users/badge.js | 6 - .../src/schema/sections/users/credits.js | 14 - .../admin/src/schema/sections/users/level.js | 8 - assets/css/frontend/forum.css | 76 - assets/css/frontend/forum/board.css | 755 -------- assets/css/frontend/forum/compose.css | 714 -------- assets/css/frontend/forum/extras.css | 7 - .../forum/extras/additional-role-badges.css | 52 - assets/css/frontend/forum/extras/base.css | 621 ------- .../css/frontend/forum/extras/like-button.css | 62 - .../css/frontend/forum/extras/load-more.css | 23 - .../frontend/forum/extras/quick-post-bar.css | 31 - .../forum/extras/reply-action-buttons.css | 110 -- .../extras/reply-header-tabs-filters.css | 69 - assets/css/frontend/forum/topic.css | 402 ---- assets/css/frontend/pages/board-cards.css | 58 - assets/css/frontend/pages/topic-rows.css | 56 - assets/js/cms-download.js | 45 +- assets/js/core/ajax-list.js | 2 +- assets/js/forum-edit.js | 47 - assets/js/forum-governance.js | 1623 ----------------- assets/js/widget-set.js | 149 -- footer.php | 10 - functions.php | 32 +- inc/bootstrap.php | 1 - inc/core-hooks/part-1.php | 335 ---- inc/helpers/aksrt-credits.php | 9 - inc/helpers/aksrt-forum-widgets.php | 363 ---- inc/helpers/aksrt-forum.php | 52 +- inc/helpers/aksrt-header-menu-buttons.php | 58 - inc/helpers/aksrt-pay.php | 33 +- inc/helpers/aksrt-runtime-win.php | 45 - inc/helpers/aksrt-user-center.php | 12 - inc/helpers/aksrt-user-home.php | 42 - inc/helpers/header/aksrt-header-navbar.php | 19 - inc/helpers/homepage/header-components.php | 16 - .../credits-points-tasks.php | 49 +- inc/modules/user-parts/routing.php | 3 - inc/modules/user-parts/template-registry.php | 14 - inc/modules/user-parts/urls.php | 11 - inc/modules/widgets-register.php | 30 - slim-forum.php | 9 - src/AKSRT/Admin/Admin.php | 2 - src/AKSRT/Admin/ForumController.php | 411 ----- src/AKSRT/Admin/ForumControllerPart1.php | 390 ---- src/AKSRT/Admin/ForumControllerPart2.php | 188 -- src/AKSRT/Admin/GlobalControllerPart1.php | 2 +- src/AKSRT/Admin/Native/PayOrders.php | 1 - src/AKSRT/Admin/Native/UsersMenus.php | 2 +- .../Admin/Schema/SchemaExtensionsAudit.php | 1 - .../Admin/Schema/SchemaExtensionsSecurity.php | 3 - src/AKSRT/Admin/Schema/SchemaForum.php | 126 -- src/AKSRT/Admin/Schema/SchemaForumBasic.php | 28 - .../Admin/Schema/SchemaForumBoardPage.php | 34 - src/AKSRT/Admin/Schema/SchemaForumCredits.php | 34 - .../Admin/Schema/SchemaForumHomepage.php | 32 - src/AKSRT/Admin/Schema/SchemaForumNames.php | 36 - src/AKSRT/Admin/Schema/SchemaForumOther.php | 46 - .../Admin/Schema/SchemaForumPermissions.php | 46 - .../Admin/Schema/SchemaForumPostPage.php | 46 - src/AKSRT/Admin/Schema/SchemaForumReply.php | 39 - src/AKSRT/Admin/Schema/SchemaForumUpload.php | 29 - src/AKSRT/Admin/Schema/SchemaPageDisplay.php | 5 +- src/AKSRT/Admin/Schema/SchemaUsers.php | 12 +- src/AKSRT/Admin/SchemaProvider.php | 6 - src/AKSRT/Admin/ToolsController.php | 7 - src/AKSRT/Content/PayRest.php | 3 +- src/AKSRT/Core/DBMigrator.php | 6 - src/AKSRT/Core/DomainModel.php | 3 - src/AKSRT/Core/ScriptsPart1b.php | 81 +- src/AKSRT/Core/ScriptsPart1c.php | 7 +- src/AKSRT/Core/ScriptsPart2.php | 30 - src/AKSRT/Core/SetupPart1.php | 23 - src/AKSRT/Core/SetupPart2.php | 3 - src/AKSRT/Core/Shortcodes.php | 23 - src/AKSRT/Core/Templates.php | 22 +- src/AKSRT/Core/WidgetAjax.php | 53 - src/AKSRT/Credits/Earn.php | 53 - src/AKSRT/Credits/PointsFree.php | 53 +- src/AKSRT/Forum/BoardService.php | 396 ---- src/AKSRT/Forum/BoardServicePart1.php | 425 ----- src/AKSRT/Forum/BoardServicePart2.php | 796 -------- src/AKSRT/Forum/BoardServicePart3.php | 187 -- src/AKSRT/Forum/CommentService.php | 604 ------ src/AKSRT/Forum/DraftService.php | 441 ----- src/AKSRT/Forum/Forum.php | 959 ---------- src/AKSRT/Forum/ModerationService.php | 717 -------- src/AKSRT/Forum/PermissionService.php | 420 ----- src/AKSRT/Forum/PlateAdminList.php | 107 -- src/AKSRT/Forum/PlateFollowPay.php | 852 --------- src/AKSRT/Forum/PlateMetaBox.php | 581 ------ src/AKSRT/Forum/ScoreService.php | 447 ----- src/AKSRT/Forum/TopicAdminList.php | 424 ----- src/AKSRT/Forum/TopicMetaBox.php | 423 ----- src/AKSRT/Forum/TopicPay.php | 853 --------- src/AKSRT/Forum/TopicService.php | 275 --- src/AKSRT/Forum/TopicServicePart1.php | 441 ----- src/AKSRT/Forum/TopicServicePart2.php | 382 ---- src/AKSRT/Forum/TopicServicePart3.php | 631 ------- src/AKSRT/Forum/TopicServicePart4.php | 306 ---- src/AKSRT/Forum/ViewBuilder.php | 718 -------- src/AKSRT/Forum/ViewBuilderPart1.php | 416 ----- src/AKSRT/Forum/ViewBuilderPart2.php | 116 -- src/AKSRT/Payment/CreatorRevenue.php | 64 +- src/AKSRT/Settings/Capabilities.php | 26 - src/AKSRT/Settings/DisplayPageGroups.php | 3 - src/AKSRT/Settings/DisplayRuntime.php | 5 - src/AKSRT/Settings/DomainGroups.php | 10 - src/AKSRT/Settings/ForumShop.php | 164 +- src/AKSRT/Settings/ThemeSearch.php | 2 - src/AKSRT/Settings/UserRuntime.php | 8 +- src/AKSRT/User/Badges.php | 19 - src/AKSRT/User/Center.php | 7 +- src/AKSRT/User/Home.php | 15 - src/AKSRT/User/Level.php | 61 - src/AKSRT/User/Metrics.php | 89 - src/AKSRT/User/Restriction.php | 17 +- src/AKSRT/Widgets/RecentCommentsWidget.php | 10 - src/AKSRT/Widgets/WidgetAuthorCard.php | 6 - src/AKSRT/Widgets/WidgetForumBoardList.php | 204 --- src/AKSRT/Widgets/WidgetForumHotTopics.php | 274 --- src/AKSRT/Widgets/WidgetForumPlateInfo.php | 174 -- src/AKSRT/Widgets/WidgetForumPlateLists.php | 284 --- .../Widgets/WidgetForumPlateModerator.php | 248 --- src/AKSRT/Widgets/WidgetForumRecentTopics.php | 109 -- src/AKSRT/Widgets/WidgetForumTabTopics.php | 188 -- src/AKSRT/Widgets/WidgetForumTopicLists.php | 292 --- src/AKSRT/Widgets/WidgetForumTopicTerms.php | 177 -- src/AKSRT/Widgets/WidgetPostPayBox.php | 2 +- src/AKSRT/Widgets/WidgetPostsNavs.php | 4 +- templates/forum/forum-board.php | 155 -- templates/forum/forum-compose.php | 463 ----- templates/forum/forum-edit.php | 362 ---- templates/forum/forum-plate-cat.php | 115 -- templates/forum/forum-plate.php | 331 ---- templates/forum/forum-tag.php | 75 - templates/forum/forum-topic.php | 850 --------- templates/pages/page-forums.php | 364 ---- templates/pages/page-user-home.php | 1 - templates/parts/user-home/panel-forum.php | 169 -- templates/parts/user-home/panels-core.php | 89 - 173 files changed, 51 insertions(+), 27360 deletions(-) delete mode 100644 assets/admin/src/constants/sections/forum.js delete mode 100644 assets/admin/src/pages/forum/ForumBoards.vue delete mode 100644 assets/admin/src/pages/forum/ForumDashboard.vue delete mode 100644 assets/admin/src/pages/forum/ForumModeratorApplications.vue delete mode 100644 assets/admin/src/pages/forum/ForumModerators.vue delete mode 100644 assets/admin/src/pages/forum/ForumTopics.vue delete mode 100644 assets/admin/src/pages/forum/components/TopicModerationDrawer.vue delete mode 100644 assets/admin/src/router/component-loaders/forum.js delete mode 100644 assets/admin/src/schema/sections/forum/basic.js delete mode 100644 assets/admin/src/schema/sections/forum/board-page.js delete mode 100644 assets/admin/src/schema/sections/forum/credits.js delete mode 100644 assets/admin/src/schema/sections/forum/homepage.js delete mode 100644 assets/admin/src/schema/sections/forum/index.js delete mode 100644 assets/admin/src/schema/sections/forum/names.js delete mode 100644 assets/admin/src/schema/sections/forum/other.js delete mode 100644 assets/admin/src/schema/sections/forum/permissions.js delete mode 100644 assets/admin/src/schema/sections/forum/post-page.js delete mode 100644 assets/admin/src/schema/sections/forum/reply.js delete mode 100644 assets/admin/src/schema/sections/forum/upload.js delete mode 100644 assets/css/frontend/forum.css delete mode 100644 assets/css/frontend/forum/board.css delete mode 100644 assets/css/frontend/forum/compose.css delete mode 100644 assets/css/frontend/forum/extras.css delete mode 100644 assets/css/frontend/forum/extras/additional-role-badges.css delete mode 100644 assets/css/frontend/forum/extras/base.css delete mode 100644 assets/css/frontend/forum/extras/like-button.css delete mode 100644 assets/css/frontend/forum/extras/load-more.css delete mode 100644 assets/css/frontend/forum/extras/quick-post-bar.css delete mode 100644 assets/css/frontend/forum/extras/reply-action-buttons.css delete mode 100644 assets/css/frontend/forum/extras/reply-header-tabs-filters.css delete mode 100644 assets/css/frontend/forum/topic.css delete mode 100644 assets/css/frontend/pages/board-cards.css delete mode 100644 assets/css/frontend/pages/topic-rows.css delete mode 100644 assets/js/forum-edit.js delete mode 100644 assets/js/forum-governance.js delete mode 100644 inc/helpers/aksrt-forum-widgets.php delete mode 100644 slim-forum.php delete mode 100644 src/AKSRT/Admin/ForumController.php delete mode 100644 src/AKSRT/Admin/ForumControllerPart1.php delete mode 100644 src/AKSRT/Admin/ForumControllerPart2.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForum.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumBasic.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumBoardPage.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumCredits.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumHomepage.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumNames.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumOther.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumPermissions.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumPostPage.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumReply.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaForumUpload.php delete mode 100644 src/AKSRT/Forum/BoardService.php delete mode 100644 src/AKSRT/Forum/BoardServicePart1.php delete mode 100644 src/AKSRT/Forum/BoardServicePart2.php delete mode 100644 src/AKSRT/Forum/BoardServicePart3.php delete mode 100644 src/AKSRT/Forum/CommentService.php delete mode 100644 src/AKSRT/Forum/DraftService.php delete mode 100644 src/AKSRT/Forum/Forum.php delete mode 100644 src/AKSRT/Forum/ModerationService.php delete mode 100644 src/AKSRT/Forum/PermissionService.php delete mode 100644 src/AKSRT/Forum/PlateAdminList.php delete mode 100644 src/AKSRT/Forum/PlateFollowPay.php delete mode 100644 src/AKSRT/Forum/PlateMetaBox.php delete mode 100644 src/AKSRT/Forum/ScoreService.php delete mode 100644 src/AKSRT/Forum/TopicAdminList.php delete mode 100644 src/AKSRT/Forum/TopicMetaBox.php delete mode 100644 src/AKSRT/Forum/TopicPay.php delete mode 100644 src/AKSRT/Forum/TopicService.php delete mode 100644 src/AKSRT/Forum/TopicServicePart1.php delete mode 100644 src/AKSRT/Forum/TopicServicePart2.php delete mode 100644 src/AKSRT/Forum/TopicServicePart3.php delete mode 100644 src/AKSRT/Forum/TopicServicePart4.php delete mode 100644 src/AKSRT/Forum/ViewBuilder.php delete mode 100644 src/AKSRT/Forum/ViewBuilderPart1.php delete mode 100644 src/AKSRT/Forum/ViewBuilderPart2.php delete mode 100644 src/AKSRT/Widgets/WidgetForumBoardList.php delete mode 100644 src/AKSRT/Widgets/WidgetForumHotTopics.php delete mode 100644 src/AKSRT/Widgets/WidgetForumPlateInfo.php delete mode 100644 src/AKSRT/Widgets/WidgetForumPlateLists.php delete mode 100644 src/AKSRT/Widgets/WidgetForumPlateModerator.php delete mode 100644 src/AKSRT/Widgets/WidgetForumRecentTopics.php delete mode 100644 src/AKSRT/Widgets/WidgetForumTabTopics.php delete mode 100644 src/AKSRT/Widgets/WidgetForumTopicLists.php delete mode 100644 src/AKSRT/Widgets/WidgetForumTopicTerms.php delete mode 100644 templates/forum/forum-board.php delete mode 100644 templates/forum/forum-compose.php delete mode 100644 templates/forum/forum-edit.php delete mode 100644 templates/forum/forum-plate-cat.php delete mode 100644 templates/forum/forum-plate.php delete mode 100644 templates/forum/forum-tag.php delete mode 100644 templates/forum/forum-topic.php delete mode 100644 templates/pages/page-forums.php delete mode 100644 templates/parts/user-home/panel-forum.php diff --git a/assets/admin/src/api/index.js b/assets/admin/src/api/index.js index 9f0fdc15..2dad120a 100644 --- a/assets/admin/src/api/index.js +++ b/assets/admin/src/api/index.js @@ -94,8 +94,7 @@ class ApiClient { async request(endpointOrConfig, options = {}) { // Dual-signature support: - // request('/forum/overview') - // request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } }) + // request('/users/1/restrictions') // request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' }) let endpoint, method, headers, params, body, rest diff --git a/assets/admin/src/constants/sections/forum.js b/assets/admin/src/constants/sections/forum.js deleted file mode 100644 index 6f3c07f0..00000000 --- a/assets/admin/src/constants/sections/forum.js +++ /dev/null @@ -1,111 +0,0 @@ -import { renderFaIcon, schemaPage } from './helpers' - -// Forum SPA = config only. Ops moved to native CPT menus (Phase 4). -export const forumSection = { - key: 'forum', - label: '社区&论坛', - icon: renderFaIcon('fas fa-comments'), - route: '/forum', - children: [ - schemaPage({ - key: 'forum-settings-basic', - name: 'ForumSettingsBasic', - label: '基础设置', - route: '/forum/settings', - title: '论坛基础设置', - desc: '论坛总开关、管理员和名称定义。', - canonicalDomain: 'forum', - section: 'basic', - }), - schemaPage({ - key: 'forum-settings-names', - name: 'ForumSettingsNames', - label: '名称定义', - route: '/forum/settings/names', - title: '名称定义', - desc: '自定义论坛各实体的显示名称。', - canonicalDomain: 'forum', - section: 'names', - }), - schemaPage({ - key: 'forum-settings-homepage', - name: 'ForumSettingsHomepage', - label: '首页设置', - route: '/forum/settings/homepage', - title: '论坛首页设置', - desc: '首页 Tab 引擎、快速发帖入口和移动端交互配置。', - canonicalDomain: 'forum', - section: 'homepage', - }), - schemaPage({ - key: 'forum-settings-board-page', - name: 'ForumSettingsBoardPage', - label: '版块页面', - route: '/forum/settings/board-page', - title: '版块页面设置', - desc: '版块页布局、Tab 栏目和移动端交互配置。', - canonicalDomain: 'forum', - section: 'board_page', - }), - schemaPage({ - key: 'forum-settings-post-page', - name: 'ForumSettingsPostPage', - label: '帖子页面', - route: '/forum/settings/post-page', - title: '帖子页面设置', - desc: '面包屑、评分、高度限制和封面配置。', - canonicalDomain: 'forum', - section: 'post_page', - }), - schemaPage({ - key: 'forum-settings-reply', - name: 'ForumSettingsReply', - label: '回复评论', - route: '/forum/settings/reply', - title: '回复评论设置', - desc: '回复翻页方式、工具栏功能和快捷回复配置。', - canonicalDomain: 'forum', - section: 'reply', - }), - schemaPage({ - key: 'forum-settings-permissions', - name: 'ForumSettingsPermissions', - label: '权限配置', - route: '/forum/settings/permissions', - title: '论坛权限配置', - desc: '发帖限制、帖子付费和版块付费关注。', - canonicalDomain: 'forum', - section: 'permissions', - }), - schemaPage({ - key: 'forum-settings-other', - name: 'ForumSettingsOther', - label: '其它设置', - route: '/forum/settings/other', - title: '论坛其它设置', - desc: '投票、版主申请说明、标题字数和 URL 别名。', - canonicalDomain: 'forum', - section: 'other', - }), - schemaPage({ - key: 'forum-settings-upload', - name: 'ForumSettingsUpload', - label: '上传权限', - route: '/forum/settings/upload', - title: '上传权限设置', - desc: '图片、视频和文件上传大小与格式限制。', - canonicalDomain: 'forum', - section: 'upload', - }), - schemaPage({ - key: 'forum-settings-credits', - name: 'ForumSettingsCredits', - label: '积分设置', - route: '/forum/settings/credits', - title: '论坛积分设置', - desc: '论坛行为积分奖励矩阵和每日上限。', - canonicalDomain: 'forum', - section: 'credits', - }), - ], -} diff --git a/assets/admin/src/constants/sections/index.js b/assets/admin/src/constants/sections/index.js index bf19d631..6a0cd22f 100644 --- a/assets/admin/src/constants/sections/index.js +++ b/assets/admin/src/constants/sections/index.js @@ -4,8 +4,6 @@ import { postListSection } from './post-list' import { permissionsSection } from './permissions' import { usersSection } from './users' import { paymentSection } from './payment' -import { shopSection } from './shop' -import { forumSection } from './forum' import { extensionsSection } from './extensions' import { themeInfoSection } from './theme-info' import { docsSection } from './docs' @@ -18,8 +16,6 @@ const SECTION_DEFAULT_CHILDREN = { permissions: 'permissions-comment', users: 'users-auth', payment: 'payment-paid', - shop: 'shop-basic', - forum: 'forum-settings-basic', extensions: 'extensions-ip', tools: 'tools-import-export', 'theme-info': 'theme-info-dashboard', @@ -47,8 +43,6 @@ export const ADMIN_SECTIONS = [ attachSectionDefaultRoute(permissionsSection), attachSectionDefaultRoute(usersSection), attachSectionDefaultRoute(paymentSection), - attachSectionDefaultRoute(shopSection), - attachSectionDefaultRoute(forumSection), attachSectionDefaultRoute(extensionsSection), attachSectionDefaultRoute(toolsSection), attachSectionDefaultRoute(themeInfoSection), diff --git a/assets/admin/src/pages/content/FloatingButtonsSettings.vue b/assets/admin/src/pages/content/FloatingButtonsSettings.vue index 83b64306..787ba723 100644 --- a/assets/admin/src/pages/content/FloatingButtonsSettings.vue +++ b/assets/admin/src/pages/content/FloatingButtonsSettings.vue @@ -372,7 +372,6 @@ const typeOptions = [ { label: '主题切换', value: 'theme_mode' }, { label: '页面二维码', value: 'qrcode' }, { label: '发布入口', value: 'add' }, - { label: '购物车', value: 'shop_cart' }, { label: 'QQ 客服', value: 'service_qq' }, { label: '微信客服', value: 'service_wechat' }, { label: '会员入口', value: 'pay_vip' }, @@ -397,7 +396,7 @@ const addPresetOptions = computed(() => { }) // pay_vip always opens the site VIP modal on the front-end — no link required. -const needsLink = computed(() => ['custom', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover) +const needsLink = computed(() => ['custom', 'add'].includes(editingItem.type) && !editingItem.hover) // Custom hover HTML only; VIP uses modal, not free-form hover content. const supportsHover = computed(() => editingItem.type === 'custom') const supportsActionMode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type)) @@ -444,7 +443,6 @@ function applyTypeDefaults(type) { service_wechat: { icon: 'fab fa-weixin', icon_dark: '', icon_light: '', text: '微信', desc: '扫码添加微信', link: '', hover: '', action_mode: 'link', qr_size: 120 }, pay_vip: { icon: 'fas fa-gem', icon_dark: '', icon_light: '', text: '会员', desc: '开通/升级会员弹窗', link: '', hover: '', action_mode: 'link', qr_size: 120 }, add: { icon: 'fas fa-plus', icon_dark: '', icon_light: '', text: '发布', desc: '发布内容', link: '', hover: '', action_mode: 'link', qr_size: 120 }, - shop_cart: { icon: 'fas fa-shopping-cart', icon_dark: '', icon_light: '', text: '购物车', desc: '购物车入口', link: '', hover: '', action_mode: 'link', qr_size: 120 }, } return map[type] || map.custom } diff --git a/assets/admin/src/pages/content/FooterTabbarSettings.vue b/assets/admin/src/pages/content/FooterTabbarSettings.vue index 0595b09e..3051cf89 100644 --- a/assets/admin/src/pages/content/FooterTabbarSettings.vue +++ b/assets/admin/src/pages/content/FooterTabbarSettings.vue @@ -186,8 +186,8 @@ - - + + @@ -312,7 +312,6 @@ const typeOptions = [ { label: '搜索', value: 'search' }, { label: '消息中心', value: 'msg' }, { label: '开通会员', value: 'pay_vip' }, - { label: '购物车', value: 'shop_cart' }, ] const typeLabelMap = { @@ -325,7 +324,6 @@ const typeLabelMap = { search: '搜索', msg: '消息中心', pay_vip: '开通会员', - shop_cart: '购物车', } const typeDefaults = { @@ -338,7 +336,6 @@ const typeDefaults = { search: { icon: 'fas fa-search', text: '搜索' }, msg: { icon: 'fas fa-bell', text: '消息' }, pay_vip: { icon: 'fas fa-crown', text: '会员' }, - shop_cart: { icon: 'fas fa-shopping-cart', text: '购物车' }, } const enabledPreviewItems = computed(() => form.items.filter((item) => item.enabled)) @@ -360,7 +357,6 @@ function getItemSummary(item) { if (item.type === 'auth') return '登录用户显示我的,游客显示登录' if (item.type === 'msg') return '跳转到消息中心页面' if (item.type === 'pay_vip') return item.link || '跳转到会员开通页' - if (item.type === 'shop_cart') return item.link || '跳转到购物车页面' return item.link || '自定义链接按钮' } @@ -405,7 +401,7 @@ function applyTypeDefaults(type, force = false) { const preset = typeDefaults[type] || typeDefaults.link if (force || !editingItem.icon) editingItem.icon = preset.icon if (force || !editingItem.text) editingItem.text = preset.text - if (!['link', 'msg', 'pay_vip', 'shop_cart'].includes(type)) editingItem.link = '' + if (!['link', 'msg', 'pay_vip'].includes(type)) editingItem.link = '' if (type !== 'category') editingItem.category_id = null if (type !== 'search') editingItem.search_default_type = 'post' if (type !== 'add') editingItem.add_items = [] @@ -483,7 +479,7 @@ function saveItem() { type: editingItem.type, icon: editingItem.icon, text: editingItem.text, - link: ['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type) ? editingItem.link : '', + link: ['link', 'msg', 'pay_vip'].includes(editingItem.type) ? editingItem.link : '', badge: (editingItem.badge || '').trim(), badge_mode: editingItem.badge_mode, category_id: editingItem.type === 'category' ? (editingItem.category_id || 0) : 0, diff --git a/assets/admin/src/pages/forum/ForumBoards.vue b/assets/admin/src/pages/forum/ForumBoards.vue deleted file mode 100644 index 7852b522..00000000 --- a/assets/admin/src/pages/forum/ForumBoards.vue +++ /dev/null @@ -1,195 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumDashboard.vue b/assets/admin/src/pages/forum/ForumDashboard.vue deleted file mode 100644 index a553b716..00000000 --- a/assets/admin/src/pages/forum/ForumDashboard.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumModeratorApplications.vue b/assets/admin/src/pages/forum/ForumModeratorApplications.vue deleted file mode 100644 index 4fd9adce..00000000 --- a/assets/admin/src/pages/forum/ForumModeratorApplications.vue +++ /dev/null @@ -1,201 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumModerators.vue b/assets/admin/src/pages/forum/ForumModerators.vue deleted file mode 100644 index 174b68e0..00000000 --- a/assets/admin/src/pages/forum/ForumModerators.vue +++ /dev/null @@ -1,363 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/ForumTopics.vue b/assets/admin/src/pages/forum/ForumTopics.vue deleted file mode 100644 index 8c1f9522..00000000 --- a/assets/admin/src/pages/forum/ForumTopics.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue b/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue deleted file mode 100644 index 55a8cfb4..00000000 --- a/assets/admin/src/pages/forum/components/TopicModerationDrawer.vue +++ /dev/null @@ -1,284 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/payment/PaymentGateway.vue b/assets/admin/src/pages/payment/PaymentGateway.vue index 0892e605..22768053 100644 --- a/assets/admin/src/pages/payment/PaymentGateway.vue +++ b/assets/admin/src/pages/payment/PaymentGateway.vue @@ -64,11 +64,11 @@ onMounted(async () => { loading.value = true try { const [gwRes, ovRes] = await Promise.all([ - api.request('/shop/gateways').catch(() => null), api.request('/payment/overview').catch(() => null), ]) - if (gwRes?.data?.gateways) { - gateways.value = gwRes.data.gateways + if (gwRes?.data?.gateway?.gateways) { + const gwList = Object.entries(gwRes.data.gateway.gateways).map(([id, enabled]) => ({ id, name: id, configured: !!enabled })) + gateways.value = gwList } if (ovRes?.data?.gateway) { overview.value = ovRes.data.gateway diff --git a/assets/admin/src/pages/tools/ToolsUpdate.vue b/assets/admin/src/pages/tools/ToolsUpdate.vue index 6057499c..fe6f396d 100644 --- a/assets/admin/src/pages/tools/ToolsUpdate.vue +++ b/assets/admin/src/pages/tools/ToolsUpdate.vue @@ -91,7 +91,6 @@ const guides = [ { key: 'meilisearch', title: 'Meilisearch', desc: '配置搜索索引与同步策略', url: '#/global/search' }, { key: 'i18n', title: '多语言翻译', desc: '检查语言切换与术语配置', url: '#/global/i18n' }, { key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' }, - { key: 'forum', title: '论坛体系', desc: '检查版块、话题与版主管理', url: '#/forum' }, { key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/security' }, { key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' }, { key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' }, diff --git a/assets/admin/src/pages/users/UsersWechatTemplate.vue b/assets/admin/src/pages/users/UsersWechatTemplate.vue index ca5e5a7b..3a157396 100644 --- a/assets/admin/src/pages/users/UsersWechatTemplate.vue +++ b/assets/admin/src/pages/users/UsersWechatTemplate.vue @@ -130,7 +130,6 @@ export default { { key: 'system_notice', desc: '系统公告通知' }, { key: 'order_paid', desc: '订单支付通知' }, { key: 'credits_change', desc: '积分变动通知' }, - { key: 'forum_reply', desc: '论坛回复通知' }, ]; const allowedTypes = ref([]); diff --git a/assets/admin/src/router/component-loaders/forum.js b/assets/admin/src/router/component-loaders/forum.js deleted file mode 100644 index d938fc9d..00000000 --- a/assets/admin/src/router/component-loaders/forum.js +++ /dev/null @@ -1,2 +0,0 @@ -// Forum ops Vue pages removed (Phase 4). Config uses SchemaPage. -export const forumPageLoaders = {} diff --git a/assets/admin/src/router/component-loaders/index.js b/assets/admin/src/router/component-loaders/index.js index d7aad84d..c38c9c28 100644 --- a/assets/admin/src/router/component-loaders/index.js +++ b/assets/admin/src/router/component-loaders/index.js @@ -2,8 +2,6 @@ import { corePageLoaders } from './core' import { usersPageLoaders } from './users' import { paymentPageLoaders } from './payment' import { toolsPageLoaders } from './tools' -import { shopPageLoaders } from './shop' -import { forumPageLoaders } from './forum' import { extensionsPageLoaders } from './extensions' export const componentLoaders = { @@ -11,7 +9,5 @@ export const componentLoaders = { ...usersPageLoaders, ...paymentPageLoaders, ...toolsPageLoaders, - ...shopPageLoaders, - ...forumPageLoaders, ...extensionsPageLoaders, } diff --git a/assets/admin/src/router/index.js b/assets/admin/src/router/index.js index cea75960..4645f8a6 100644 --- a/assets/admin/src/router/index.js +++ b/assets/admin/src/router/index.js @@ -76,18 +76,6 @@ function createSectionRedirectRoutes() { * Bookmarks under #/shop/products etc. land on NativeOpsRedirect. */ const NATIVE_OPS_REDIRECTS = [ - { path: '/shop/products', title: '商品管理', nativeAdminPath: 'edit.php?post_type=aksrt_product' }, - { path: '/shop/orders', title: '订单管理', nativeAdminPath: 'admin.php?page=aksrt_orders' }, - { path: '/shop/after-sale', title: '售后管理', nativeAdminPath: 'admin.php?page=aksrt_after_sale' }, - { path: '/shop/shipping', title: '物流管理', nativeAdminPath: 'admin.php?page=aksrt_shipping' }, - { path: '/shop/coupons', title: '优惠券', nativeAdminPath: 'admin.php?page=aksrt_coupons' }, - { path: '/shop/withdrawals', title: '提现管理', nativeAdminPath: 'admin.php?page=aksrt_withdraw' }, - { path: '/shop/dashboard', title: '商城概览', nativeAdminPath: 'admin.php?page=aksrt_pay' }, - { path: '/forum/boards', title: '板块管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/topics', title: '主题治理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' }, - { path: '/forum/moderators', title: '版主管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/moderator-applications', title: '版主申请', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' }, - { path: '/forum/dashboard', title: '论坛概览', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' }, { path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' }, { path: '/payment/revenue', title: '创作分成', nativeAdminPath: 'admin.php?page=aksrt_income' }, { path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' }, diff --git a/assets/admin/src/schema/index.js b/assets/admin/src/schema/index.js index 64f20229..5e20efc2 100644 --- a/assets/admin/src/schema/index.js +++ b/assets/admin/src/schema/index.js @@ -34,8 +34,6 @@ import { usersSchemas } from './sections/users/index.js' import { paymentSchemas } from './sections/payment/index.js' import { extensionsSchemas } from './sections/extensions/index.js' import { toolsSchemas } from './sections/tools/index.js' -import { shopSchemas } from './sections/shop/index.js' -import { forumSchemas } from './sections/forum/index.js' export function normalizeSchemaDomain(domain = '') { const value = String(domain).trim() @@ -55,8 +53,6 @@ export const CANONICAL_SCHEMA_DOMAINS = [ 'payment', 'extensions', 'tools', - 'shop', - 'forum', ] /** All schemas organized by domain */ @@ -69,8 +65,6 @@ export const ALL_SCHEMAS = Object.freeze({ payment: paymentSchemas, extensions: extensionsSchemas, tools: toolsSchemas, - shop: shopSchemas, - forum: forumSchemas, }) /** diff --git a/assets/admin/src/schema/sections/extensions/audit.js b/assets/admin/src/schema/sections/extensions/audit.js index c0df6ec9..06e9ef7e 100644 --- a/assets/admin/src/schema/sections/extensions/audit.js +++ b/assets/admin/src/schema/sections/extensions/audit.js @@ -42,7 +42,6 @@ export const auditSchema = { fields: [ { key: 'content_audit_text_enabled', label: '文本审核', type: 'switch', default: true, dependsOn: { key: 'content_audit_enabled', value: true } }, { key: 'content_audit_image_enabled', label: '图片审核', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } }, - { key: 'audit_bbs_term', label: '审核论坛分类', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } }, ], }, { diff --git a/assets/admin/src/schema/sections/forum/basic.js b/assets/admin/src/schema/sections/forum/basic.js deleted file mode 100644 index c43a8f14..00000000 --- a/assets/admin/src/schema/sections/forum/basic.js +++ /dev/null @@ -1,28 +0,0 @@ -export const basicSchema = { - key: 'forum.basic', - title: '基础设置', - icon: 'fas fa-power-off', - description: '论坛总开关与管理员配置。', - defaults: { - bbs_s: true, - bbs_admin_users: [], - }, - groups: [ - { - key: 'switch', - title: '功能开关', - icon: 'fas fa-toggle-on', - fields: [ - { key: 'bbs_s', label: '启用论坛', type: 'switch', default: true, help: '关闭后论坛页面将返回 404' }, - ], - }, - { - key: 'admin', - title: '论坛管理', - icon: 'fas fa-user-shield', - fields: [ - { key: 'bbs_admin_users', label: '论坛管理员', type: 'user_select', multiple: true, default: [], help: '拥有跨版块全部治理权限,绕过 ban' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/board-page.js b/assets/admin/src/schema/sections/forum/board-page.js deleted file mode 100644 index 40133c99..00000000 --- a/assets/admin/src/schema/sections/forum/board-page.js +++ /dev/null @@ -1,31 +0,0 @@ -export const boardPageSchema = { - key: 'forum.board_page', - title: '版块页面', - icon: 'fas fa-th-large', - description: '版块页布局、Tab 栏目和移动端交互配置。', - defaults: { - bbs_plate_top_info_s: true, - bbs_plate_tab: [ - { label: '全部', sort: 'date' }, - { label: '推荐', sort: 'featured' }, - { label: '最新回复', sort: 'last_reply' }, - { label: '热门', sort: 'hot' }, - { label: '精华', sort: 'essence' }, - ], - bbs_plate_tab_active_index: 0, - bbs_plate_tab_swiper: true, - }, - groups: [ - { - key: 'layout', - title: '版块页布局', - icon: 'fas fa-layer-group', - 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 }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/credits.js b/assets/admin/src/schema/sections/forum/credits.js deleted file mode 100644 index ee38c282..00000000 --- a/assets/admin/src/schema/sections/forum/credits.js +++ /dev/null @@ -1,37 +0,0 @@ -export const creditsSchema = { - key: 'forum.credits', - title: '积分设置', - icon: 'fas fa-coins', - description: '论坛行为积分奖励矩阵和每日积分上限。', - defaults: { - bbs_credits_post_new: 5, - bbs_credits_plate_new: 10, - bbs_credits_comment_new: 2, - bbs_credits_comment_like: 1, - bbs_credits_score_extra: 3, - bbs_credits_essence: 10, - bbs_credits_posts_hot: 5, - bbs_hot_recommend_threshold: 500, - bbs_credits_adopted: 20, - bbs_credits_daily_max: 50, - }, - groups: [ - { - key: 'earn', - title: '积分奖励', - icon: 'fas fa-gift', - 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_hot_recommend_threshold', label: '热帖推荐分阈值', type: 'number', default: 500, min: 1, help: '推荐分达到该值触发热帖奖励与勋章累计' }, - { 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 = 不限' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/homepage.js b/assets/admin/src/schema/sections/forum/homepage.js deleted file mode 100644 index e36b0c2a..00000000 --- a/assets/admin/src/schema/sections/forum/homepage.js +++ /dev/null @@ -1,29 +0,0 @@ -export const homepageSchema = { - key: 'forum.homepage', - title: '首页设置', - icon: 'fas fa-home', - description: '论坛首页 Tab 引擎、快速发帖入口和移动端交互配置。', - defaults: { - bbs_home_tab: [ - { type: 'follow', label: '关注' }, - { type: 'synthesis', label: '综合' }, - { type: 'plate', label: '版块' }, - ], - bbs_home_tab_active_index: 1, - bbs_home_tab_quick_posts_s: true, - bbs_home_tab_swiper: true, - }, - groups: [ - { - key: 'tabs', - title: '首页 Tab', - icon: 'fas fa-columns', - 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 }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/index.js b/assets/admin/src/schema/sections/forum/index.js deleted file mode 100644 index d0396a9d..00000000 --- a/assets/admin/src/schema/sections/forum/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Forum domain schemas. - * - * 12 sections matching PHP schema_forum(): - * basic / names / homepage / board_page / post_page / reply / - * permissions / other / upload / credits - */ - -import { basicSchema } from './basic.js' -import { namesSchema } from './names.js' -import { homepageSchema } from './homepage.js' -import { boardPageSchema } from './board-page.js' -import { postPageSchema } from './post-page.js' -import { replySchema } from './reply.js' -import { permissionsSchema } from './permissions.js' -import { otherSchema } from './other.js' -import { uploadSchema } from './upload.js' -import { creditsSchema } from './credits.js' - -export const forumSchemas = { - basic: basicSchema, - names: namesSchema, - homepage: homepageSchema, - board_page: boardPageSchema, - post_page: postPageSchema, - reply: replySchema, - permissions: permissionsSchema, - other: otherSchema, - upload: uploadSchema, - credits: creditsSchema, -} diff --git a/assets/admin/src/schema/sections/forum/names.js b/assets/admin/src/schema/sections/forum/names.js deleted file mode 100644 index 80f99d72..00000000 --- a/assets/admin/src/schema/sections/forum/names.js +++ /dev/null @@ -1,41 +0,0 @@ -export const namesSchema = { - key: 'forum.names', - title: '名称定义', - icon: 'fas fa-language', - description: '自定义论坛各实体的显示名称(12 词条)。', - defaults: { - bbs_forum_display_name: '论坛', - bbs_plate_display_name: '版块', - bbs_posts_display_name: '帖子', - bbs_topic_display_name: '话题', - bbs_tag_display_name: '标签', - bbs_comment_display_name: '回复', - bbs_question_badge_name: '提问', - bbs_question_ok_badge_name: '已解决', - bbs_cat_moderator_name: '分区版主', - bbs_plate_author_name: '超级版主', - bbs_plate_moderator_name: '版主', - bbs_plate_follow_name: '关注', - }, - groups: [ - { - key: 'display_names', - title: '名词自定义', - icon: 'fas fa-tags', - fields: [ - { key: 'bbs_forum_display_name', label: '论坛', type: 'input', default: '论坛' }, - { key: 'bbs_plate_display_name', label: '版块', type: 'input', default: '版块' }, - { key: 'bbs_posts_display_name', label: '帖子', type: 'input', default: '帖子' }, - { key: 'bbs_topic_display_name', label: '话题', type: 'input', default: '话题' }, - { key: 'bbs_tag_display_name', label: '标签', type: 'input', default: '标签' }, - { key: 'bbs_comment_display_name', label: '回复', type: 'input', default: '回复' }, - { key: 'bbs_question_badge_name', label: '提问徽章', type: 'input', default: '提问' }, - { key: 'bbs_question_ok_badge_name', label: '已解决徽章', type: 'input', default: '已解决' }, - { key: 'bbs_cat_moderator_name', label: '分区版主', type: 'input', default: '分区版主' }, - { key: 'bbs_plate_author_name', label: '超级版主', type: 'input', default: '超级版主' }, - { key: 'bbs_plate_moderator_name', label: '版主', type: 'input', default: '版主' }, - { key: 'bbs_plate_follow_name', label: '关注', type: 'input', default: '关注' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/other.js b/assets/admin/src/schema/sections/forum/other.js deleted file mode 100644 index d0c8c20a..00000000 --- a/assets/admin/src/schema/sections/forum/other.js +++ /dev/null @@ -1,64 +0,0 @@ -export const otherSchema = { - key: 'forum.other', - title: '其它设置', - icon: 'fas fa-cog', - description: '投票、版主申请说明、标题字数限制和 URL 别名配置。', - defaults: { - bbs_vote_number_type: 'percent', - bbs_vote_max: 8, - bbs_apply_moderator_desc: '欢迎有热情的用户申请成为版主,协助管理社区秩序。', - bbs_term_thumb: '', - bbs_post_lists_hide_pay_s: false, - bbs_post_title_strlen_limit: 30, - bbs_t_placeholder: '请输入标题', - bbs_c_placeholder: '请输入内容', - bbs_rewrite_suffix_html_s: true, - bbs_plate_rewrite_slug: 'board', - bbs_posts_rewrite_slug: 'topic', - bbs_posts_edit_rewrite_slug: 'topic-edit', - bbs_posts_show_in_rest_s: false, - }, - groups: [ - { - key: 'vote', - title: '投票', - icon: 'fas fa-poll', - 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: '版主申请', - icon: 'fas fa-user-plus', - fields: [ - { key: 'bbs_apply_moderator_desc', label: '申请说明', type: 'textarea', default: '欢迎有热情的用户申请成为版主,协助管理社区秩序。' }, - ], - }, - { - key: 'misc', - title: '杂项', - icon: 'fas fa-sliders-h', - 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: 'input', default: '请输入标题' }, - { key: 'bbs_c_placeholder', label: '内容占位符', type: 'input', default: '请输入内容' }, - ], - }, - { - key: 'url', - title: 'URL / 别名', - icon: 'fas fa-link', - fields: [ - { key: 'bbs_rewrite_suffix_html_s', label: '.html 后缀', type: 'switch', default: true }, - { key: 'bbs_plate_rewrite_slug', label: '版块别名', type: 'input', default: 'board' }, - { key: 'bbs_posts_rewrite_slug', label: '帖子别名', type: 'input', default: 'topic' }, - { key: 'bbs_posts_edit_rewrite_slug', label: '编辑别名', type: 'input', default: 'topic-edit' }, - { key: 'bbs_posts_show_in_rest_s', label: '古腾堡编辑器', type: 'switch', default: false, help: '开启后帖子可使用 Gutenberg' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/permissions.js b/assets/admin/src/schema/sections/forum/permissions.js deleted file mode 100644 index 25c227c2..00000000 --- a/assets/admin/src/schema/sections/forum/permissions.js +++ /dev/null @@ -1,64 +0,0 @@ -export const permissionsSchema = { - key: 'forum.permissions', - title: '权限配置', - icon: 'fas fa-lock', - description: '发帖限制、按钮显示、帖子付费和版块付费关注配置。', - defaults: { - bbs_posts_add_limit_opt_max: 10, - bbs_plate_add_limit_opt_max: 3, - bbs_show_new_plate: true, - bbs_show_new_topic: true, - bbs_show_apply_moderator: true, - bbs_post_pay_hide_type_s: true, - bbs_post_pay_vip_price_s: true, - bbs_post_pay_vip_1_discount: 80, - bbs_post_pay_vip_2_discount: 50, - bbs_post_pay_price_limit: 1000, - bbs_plate_follow_pay_s: false, - bbs_plate_follow_pay_price: 10, - bbs_plate_follow_pay_type: 'money', - }, - groups: [ - { - key: 'posting', - title: '发帖限制', - icon: 'fas fa-ban', - 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: '按钮显示', - icon: 'fas fa-eye', - fields: [ - { key: 'bbs_show_new_plate', label: '始终显示建版按钮', type: 'switch', default: true }, - { key: 'bbs_show_new_topic', label: '始终显示发帖按钮', type: 'switch', default: true }, - { key: 'bbs_show_apply_moderator', label: '始终显示申请版主', type: 'switch', default: true }, - ], - }, - { - key: 'paid_content', - title: '帖子付费', - icon: 'fas fa-money-bill-wave', - fields: [ - { key: 'bbs_post_pay_hide_type_s', label: '启用付费内容', type: 'switch', default: true }, - { key: 'bbs_post_pay_vip_price_s', label: 'VIP 特价', type: 'switch', default: true, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } }, - { key: 'bbs_post_pay_vip_1_discount', label: '一级 VIP 折扣 (%)', type: 'number', default: 80, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } }, - { key: 'bbs_post_pay_vip_2_discount', label: '二级 VIP 折扣 (%)', type: 'number', default: 50, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } }, - { key: 'bbs_post_pay_price_limit', label: '最高定价', type: 'number', default: 1000, min: 1, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } }, - ], - }, - { - key: 'plate_follow', - title: '版块付费关注', - icon: 'fas fa-heart', - 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, dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } }, - { key: 'bbs_plate_follow_pay_type', label: '支付方式', type: 'select', default: 'money', options: [{ label: '现金', value: 'money' }, { label: '积分', value: 'points' }], dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/post-page.js b/assets/admin/src/schema/sections/forum/post-page.js deleted file mode 100644 index 3cd1aa2a..00000000 --- a/assets/admin/src/schema/sections/forum/post-page.js +++ /dev/null @@ -1,60 +0,0 @@ -export const postPageSchema = { - key: 'forum.post_page', - title: '帖子页面', - icon: 'fas fa-file-alt', - description: '帖子页面包屑、评分、高度限制、封面比例和视频静音配置。', - defaults: { - bbs_breadcrumbs_s: true, - bbs_breadcrumbs_home: true, - bbs_breadcrumbs_bbs_home: true, - bbs_breadcrumbs_bbs_home_name: '论坛', - bbs_breadcrumbs_plate_cat: true, - bbs_score_extra_max: 5, - bbs_score_deduct_max: 3, - bbs_posts_maxheight_s: false, - bbs_posts_maxheight: 800, - bbs_posts_cover_ratio: '16:9', - bbs_posts_cover_video_mute: true, - }, - groups: [ - { - key: 'breadcrumbs', - title: '面包屑', - icon: 'fas fa-angle-double-right', - fields: [ - { key: 'bbs_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true }, - { key: 'bbs_breadcrumbs_home', label: '显示首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } }, - { key: 'bbs_breadcrumbs_bbs_home', label: '显示论坛首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } }, - { key: 'bbs_breadcrumbs_bbs_home_name', label: '论坛首页名称', type: 'input', default: '论坛', dependsOn: { key: 'bbs_breadcrumbs_bbs_home', value: true } }, - { key: 'bbs_breadcrumbs_plate_cat', label: '显示分区', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } }, - ], - }, - { - key: 'score', - title: '评分设置', - icon: 'fas fa-star', - 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: '内容显示', - icon: 'fas fa-expand-arrows-alt', - 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, dependsOn: { key: 'bbs_posts_maxheight_s', value: true } }, - ], - }, - { - key: 'cover', - title: '封面设置', - icon: 'fas fa-image', - 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 }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/reply.js b/assets/admin/src/schema/sections/forum/reply.js deleted file mode 100644 index e9235b3d..00000000 --- a/assets/admin/src/schema/sections/forum/reply.js +++ /dev/null @@ -1,44 +0,0 @@ -export const replySchema = { - key: 'forum.reply', - title: '回复评论', - icon: 'fas fa-reply-all', - description: '回复翻页、工具栏功能、表情、代码和快捷回复配置。', - defaults: { - bbs_reply_paginate_type: 'ajax', - bbs_reply_paginate_ias_s: true, - bbs_reply_paginate_ias_max: 3, - bbs_comment_smilie: true, - bbs_comment_code: true, - bbs_comment_img: true, - bbs_comment_upload_img: true, - bbs_comment_quick_s: true, - bbs_comment_quick_often: '不错,学习了\n感谢分享\n期待更新', - bbs_comment_placeholder: '写下你的回复…', - }, - groups: [ - { - key: 'paginate', - title: '翻页', - icon: 'fas fa-chevron-circle-right', - 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: true }, - { key: 'bbs_reply_paginate_ias_max', label: '自动加载页数', type: 'number', default: 3, min: 0, max: 10, dependsOn: { key: 'bbs_reply_paginate_ias_s', value: true } }, - ], - }, - { - key: 'toolbar', - title: '回复工具栏', - icon: 'fas fa-toolbox', - 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: true }, - { key: 'bbs_comment_quick_s', label: '快捷回复', type: 'switch', default: true }, - { key: 'bbs_comment_quick_often', label: '预设短语', type: 'textarea', default: '不错,学习了\n感谢分享\n期待更新', help: '每行一条', dependsOn: { key: 'bbs_comment_quick_s', value: true } }, - { key: 'bbs_comment_placeholder', label: '回复占位符', type: 'input', default: '写下你的回复…' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/forum/upload.js b/assets/admin/src/schema/sections/forum/upload.js deleted file mode 100644 index df879eaa..00000000 --- a/assets/admin/src/schema/sections/forum/upload.js +++ /dev/null @@ -1,27 +0,0 @@ -export const uploadSchema = { - key: 'forum.upload', - title: '上传权限', - icon: 'fas fa-cloud-upload-alt', - description: '论坛帖子图片、视频和文件的上传大小与格式限制。', - defaults: { - bbs_upload_img_size: 2048, - bbs_image_upload_multiple: 9, - bbs_upload_video_size: 20480, - bbs_upload_file_size: 10240, - bbs_upload_file_mimes: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', - }, - groups: [ - { - key: 'limits', - title: '上传限制', - icon: 'fas fa-weight-hanging', - 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: 'input', default: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', help: '逗号分隔' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/global/search.js b/assets/admin/src/schema/sections/global/search.js index 3ae14cf7..4b6fe809 100644 --- a/assets/admin/src/schema/sections/global/search.js +++ b/assets/admin/src/schema/sections/global/search.js @@ -25,8 +25,6 @@ export const searchSchema = { search_type_user: false, search_type_category: true, search_type_tag: true, - search_type_product: false, - search_type_topic: false, search_default_type: 'post', search_ms_enabled: false, search_ms_api_url: '', @@ -63,8 +61,6 @@ export const searchSchema = { { 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: '默认类型', @@ -75,8 +71,6 @@ export const searchSchema = { { label: '用户', value: 'user' }, { label: '分类', value: 'category' }, { label: '标签', value: 'tag' }, - { label: '商品', value: 'product' }, - { label: '帖子', value: 'topic' }, ], }, ], diff --git a/assets/admin/src/schema/sections/page-display/index.js b/assets/admin/src/schema/sections/page-display/index.js index 0e0f45f3..27de3175 100644 --- a/assets/admin/src/schema/sections/page-display/index.js +++ b/assets/admin/src/schema/sections/page-display/index.js @@ -136,15 +136,9 @@ export const pageDisplaySchemas = { ], dependsOn: { key: 'header_newposts_enabled', value: true } }, { key: 'header_newposts_types', label: '发布类型', type: 'checkbox', default: ['post'], options: [ { label: '文章', value: 'post' }, - { label: '帖子', value: 'topic' }, - { label: '商品', value: 'product' }, - { label: '版块', value: 'plate' }, ], dependsOn: { key: 'header_newposts_enabled', value: true }, help: '未开启的模块不会显示' }, { key: 'header_user_newposts_btn', label: '用户卡发布类型', type: 'select', default: 'post', options: [ { label: '文章', value: 'post' }, - { label: '帖子', value: 'topic' }, - { label: '商品', value: 'product' }, - { label: '版块', value: 'plate' }, ] }, { key: 'header_user_newposts_text', label: '用户卡发布文案', type: 'input', default: '发布内容' }, { key: 'header_nav_pay_vip', label: '导航栏开通会员按钮', type: 'switch', default: true, help: '顶部导航显示开通会员' }, @@ -349,7 +343,7 @@ export const pageDisplaySchemas = { title: 'Tab 配置', icon: 'fas fa-columns', fields: [ - { key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum' }, + { key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits' }, { key: 'user_home_tabs_order', label: 'Tab 显示顺序', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔,控制栏目顺序' }, { key: 'user_home_tabs_pc_hide', label: 'PC 端隐藏', type: 'input', default: '', help: '逗号分隔要在 PC 端隐藏的栏目' }, { key: 'user_home_tabs_mobile_hide', label: '移动端隐藏', type: 'input', default: '', help: '逗号分隔要在移动端隐藏的栏目' }, @@ -669,9 +663,6 @@ export const pageDisplaySchemas = { { label: '用户页', value: 'author' }, { label: '文章页', value: 'single' }, { label: '搜索页', value: 'search' }, - { label: '[论坛]首页', value: 'forum_home' }, - { label: '[论坛]板块页面', value: 'forum_plate' }, - { label: '[论坛]帖子页面', value: 'forum_post' }, { label: '其它页面', value: 'page' }, ], help: '选择启用顶部多功能组件的页面类型,一个都不选则为关闭此功能' }, { key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [ @@ -717,13 +708,10 @@ export const pageDisplaySchemas = { { key: 'header_components_search_popular_limit', label: '关键词最多显示', type: 'number', default: 6, min: 2, max: 50, dependsOn: { key: 'header_components_search_show_keywords', value: true } }, { key: 'header_components_search_show_input_cat', label: '分类搜索', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示分类搜索选择' }, { key: 'header_components_search_in_cat', label: '默认搜索的分类', type: 'input', default: '', dependsOn: { key: 'header_components_search_show_input_cat', value: true }, help: '分类 ID,可留空' }, - { key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户、版块、帖子、商品)' }, + { key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户)' }, { key: 'header_components_search_in_type', label: '默认搜索类型', type: 'radio', default: 'post', dependsOn: { key: 'header_components_search_show_type', value: true }, options: [ { label: '文章', value: 'post' }, { label: '用户', value: 'user' }, - { label: '商品', value: 'product' }, - { label: '帖子', value: 'forum' }, - { label: '版块', value: 'plate' }, ]}, { key: 'header_components_search_before_html', diff --git a/assets/admin/src/schema/sections/users/badge.js b/assets/admin/src/schema/sections/users/badge.js index 7d1f73cb..9e12cdb1 100644 --- a/assets/admin/src/schema/sections/users/badge.js +++ b/assets/admin/src/schema/sections/users/badge.js @@ -7,16 +7,10 @@ export const MEDAL_GET_TYPES = [ { label: '注册天数', value: 'registration_time' }, { label: '累计签到天数', value: 'checkin_all_day' }, { label: '发布文章数', value: 'new_post' }, - { label: '论坛发帖数', value: 'bbs_post' }, { label: '评论数', value: 'comment' }, { label: '粉丝数', value: 'followed' }, { label: '文章获赞', value: 'like' }, { label: '评论获赞', value: 'comment_like' }, - { label: '回复被采纳', value: 'bbs_adopt' }, - { label: '神评次数', value: 'bbs_comment_hot' }, - { label: '热帖次数', value: 'bbs_post_hot' }, - { label: '精华帖数', value: 'bbs_essence' }, - { label: '帖子评分累计', value: 'bbs_score' }, { label: '内容阅读量', value: 'views' }, { label: '创作分成累计(元)', value: 'pay_income' }, { label: '推广佣金累计(元)', value: 'pay_rebate' }, diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index 09ff0727..637c9a81 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -19,13 +19,6 @@ export const creditsSchema = { points_free_comment_new: 1, points_free_comment_like: 1, points_free_followed: 1, - points_free_bbs_posts_new: 5, - points_free_bbs_reply: 2, - points_free_bbs_essence: 10, - points_free_bbs_adopt: 20, - points_free_bbs_posts_hot: 5, - points_free_bbs_comment_hot: 5, - points_free_bbs_score_extra: 3, points_earn_invite: 10, points_spend_comment: 0, points_spend_download: 0, @@ -74,13 +67,6 @@ export const creditsSchema = { { key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 }, { key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 }, { key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 }, - { key: 'points_free_bbs_posts_new', label: '论坛发帖', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_reply', label: '论坛回复', type: 'number', default: 2, min: 0, max: 1000 }, - { key: 'points_free_bbs_essence', label: '帖子加精', type: 'number', default: 10, min: 0, max: 1000 }, - { key: 'points_free_bbs_adopt', label: '回复被采纳', type: 'number', default: 20, min: 0, max: 1000 }, - { key: 'points_free_bbs_posts_hot', label: '热帖奖励', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_comment_hot', label: '神评奖励', type: 'number', default: 5, min: 0, max: 1000 }, - { key: 'points_free_bbs_score_extra', label: '帖子加分', type: 'number', default: 3, min: 0, max: 1000 }, { key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' }, ], }, diff --git a/assets/admin/src/schema/sections/users/level.js b/assets/admin/src/schema/sections/users/level.js index 22003336..f0c38ba4 100644 --- a/assets/admin/src/schema/sections/users/level.js +++ b/assets/admin/src/schema/sections/users/level.js @@ -11,14 +11,6 @@ export const INTEGRAL_CATALOG = [ { key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' }, { key: 'comment_new', label: '发表评论', default: 2, group: '文章' }, { key: 'comment_like', label: '评论获赞', default: 1, group: '文章' }, - { key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' }, - { key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' }, - { key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' }, - { key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' }, - { key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' }, - { key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' }, - { key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' }, - { key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' }, ] export function defaultLevelOpt(max = 10) { diff --git a/assets/css/frontend/forum.css b/assets/css/frontend/forum.css deleted file mode 100644 index 65e303ec..00000000 --- a/assets/css/frontend/forum.css +++ /dev/null @@ -1,76 +0,0 @@ -@import url("forum/board.css"); -@import url("forum/topic.css"); -@import url("forum/compose.css"); -@import url("forum/extras.css"); - - -/* Forum home shell: sidebar compose CTA */ -.aksrt-forum-sidebar-card--compose { - padding: 12px; -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 10px 12px; - border-radius: 10px; - background: var(--body-bg-color, #f5f6f7); - color: var(--muted-color, #777); - text-decoration: none; -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input:hover { - color: var(--focus-color, #f04494); -} -.aksrt-forum-sidebar-card--compose .aksrt-forum-header__action { - display: inline-flex; - width: 100%; - justify-content: center; -} -.aksrt-forum-header__stats { - display: none; /* stats demoted on home shell; counts remain in eyebrow meta */ -} - - -/* Forum pages share the single/archive content stream shell */ -.content-shell--forum .content-wrap { - padding-top: 8px; -} -.content-shell--forum .content-layout__primary > .aksrt-forum-board, -.content-shell--forum .content-layout__primary > .aksrt-forum-topic, -.content-shell--forum .content-layout__primary > .aksrt-forum-tag, -.content-shell--forum .content-layout__primary > .aksrt-forum-plate-cat { - width: 100%; -} - - -.aksrt-forum-topic__cover { - margin: 0 0 16px; - border-radius: var(--main-radius, 8px); - overflow: hidden; - background: var(--body-bg-color, #f5f6f7); -} -.aksrt-forum-topic__cover--image { - aspect-ratio: var(--cover-ratio, 16/9); -} -.aksrt-forum-topic__cover img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} -.aksrt-forum-topic__cover--slide { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 100%; - overflow-x: auto; - scroll-snap-type: x mandatory; -} -.aksrt-forum-topic__cover--slide img { - scroll-snap-align: start; - aspect-ratio: 16/9; -} -.aksrt-forum-topic__paywall { - margin: 12px 0 20px; - padding: 16px; -} diff --git a/assets/css/frontend/forum/board.css b/assets/css/frontend/forum/board.css deleted file mode 100644 index df23dd1c..00000000 --- a/assets/css/frontend/forum/board.css +++ /dev/null @@ -1,755 +0,0 @@ -/* Forum home-shell refinement */ -.aksrt-forum-board__hero { - margin-bottom: 20px; - padding: 24px; - border-radius: var(--radius-2xl, 16px); - background: linear-gradient(180deg, rgba(240,68,148,0.05), rgba(255,255,255,0.96)); - border: none; - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__summary { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 16px; -} - -.aksrt-forum-board__summary-item { - min-width: 120px; - padding: 10px 14px; - border-radius: 999px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); -} - -.aksrt-forum-board__summary-label { - display: block; - margin-bottom: 4px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__layout { - align-items: start; -} - -.aksrt-forum-board__governance--inline { - margin-bottom: 20px; -} - -.aksrt-forum-board__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -.aksrt-forum-board__governance-card--sidebar { - position: sticky; - top: calc(var(--nav-height, 64px) + 16px); -} - -.aksrt-forum-board__moderators--stacked { - flex-direction: column; - align-items: stretch; -} - -.aksrt-forum-board { - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-board__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - margin-bottom: 24px; - padding: 22px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-forum-board__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__meta { - display: flex; - gap: 16px; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__meta i { - margin-right: 4px; -} - -.aksrt-forum-board__compose-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 18px; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - text-decoration: none; - font-size: 14px; - font-weight: 500; - white-space: nowrap; - transition: opacity 0.15s; -} - -.aksrt-forum-board__compose-btn:hover { - opacity: 0.85; -} - -.aksrt-forum-board__actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: flex-end; -} - -.aksrt-forum-board__manage-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 8px; - border: 1px solid var(--color-line, #d9d9d9); - background: var(--surface-1, #fff); - color: var(--color-ink); - text-decoration: none; - cursor: pointer; - font-size: 13px; -} - -.aksrt-forum-board__manage-btn--danger { - border-color: var(--color-error, #f5222d); - color: var(--color-error, #f5222d); -} - -.aksrt-forum-board__manage-btn--danger:hover { - background: var(--color-error, #f5222d); - color: #fff; -} - -.aksrt-forum-board__governance { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 16px; - margin-bottom: 20px; -} - -.aksrt-forum-board__governance-card { - padding: 18px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__section-title { - margin: 0 0 10px; - font-size: 15px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-forum-board__moderators { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.aksrt-forum-board__moderator { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - border-radius: 999px; - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-board__moderator img { - width: 36px; - height: 36px; - border-radius: 50%; -} - -.aksrt-forum-board__application-form textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - resize: vertical; - font-family: inherit; -} - -.aksrt-forum-board__application-actions { - display: flex; - align-items: center; - gap: 10px; - margin-top: 10px; -} - -.aksrt-forum-board__apply-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 8px 14px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; -} - -.aksrt-forum-board__application-note { - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__application-reason { - margin: 10px 0; - padding: 10px 12px; - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); - background: var(--surface-2, #f8fafc); - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-board__governance-card .aksrt-forum-board__apply-btn { - text-decoration: none; -} - -/* Sticky section */ -.aksrt-forum-board__sticky { - margin-bottom: 16px; - padding: 12px 16px; - background: var(--surface-1, #fff); - border-radius: 8px; - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); -} - -.aksrt-forum-board__sticky h3 { - margin: 0 0 8px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-board__sticky-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 0; - text-decoration: none; - color: var(--color-ink); - font-size: 14px; -} - -.aksrt-forum-board__sticky-item:hover { - color: var(--color-primary); -} - -.aksrt-forum-board__sticky-meta { - font-size: 12px; - color: var(--color-ink-4, #c0c4cc); -} - -.aksrt-forum-board__sticky-badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 11px; - font-weight: 500; - background: var(--color-primary); - color: #fff; - margin-right: 6px; - flex-shrink: 0; -} - -/* Topic list items */ -.aksrt-forum-board__topics { - display: flex; - flex-direction: column; - gap: 1px; -} - -.aksrt-forum-topic-item { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 18px; - background: var(--surface-1, #fff); - border-bottom: 1px solid var(--color-line-soft); - transition: background 0.15s, box-shadow 0.15s; -} - -.aksrt-forum-topic-item:first-child { - border-radius: 8px 8px 0 0; -} - -.aksrt-forum-topic-item:last-child { - border-radius: 0 0 8px 8px; -} - -.aksrt-forum-topic-item:hover { - background: var(--surface-4, #f8f9fa); -} - -.aksrt-forum-topic-item.is-sticky { - background: var(--surface-2, #f0f5ff); -} - -.aksrt-forum-topic-item.is-featured { - border-left: 3px solid var(--color-success, #52c41a); -} - -.aksrt-forum-topic-item__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; -} - -.aksrt-forum-topic-item__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-item__title { - font-size: 15px; - font-weight: 500; - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic-item__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-item__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-item__badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 11px; - font-weight: 500; -} - -.aksrt-forum-topic-item__badge--sticky { - background: var(--color-primary); - color: #fff; -} - -/* 置顶分级视觉区分(§7.3):超级置顶加深色,全局置顶用红色凸显。 */ -.aksrt-forum-topic-item__badge--sticky-super { - background: var(--focus-color, #fd53a1); - color: #fff; -} - -.aksrt-forum-topic-item__badge--sticky-global { - background: #e53e3e; - color: #fff; -} - -.aksrt-forum-topic-item__badge--featured { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-topic-item__badge--muted { - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic-item__lock { - color: var(--color-warning, #fa8c16); - font-size: 12px; -} - -.aksrt-forum-topic-item__meta { - display: flex; - gap: 12px; - margin-top: 3px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-item__stats { - display: flex; - gap: 12px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-item__stats i { - margin-right: 3px; -} - -.aksrt-forum-topic-item__actions { - display: flex; - flex-direction: column; - gap: 6px; -} - -.aksrt-forum-topic-item__action { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 6px 10px; - border-radius: 999px; - border: 1px solid var(--color-line, #d9d9d9); - background: #fff; - color: var(--color-ink); - text-decoration: none; - cursor: pointer; - font-size: 12px; - white-space: nowrap; -} - -.aksrt-forum-topic-item__action--link { - background: var(--surface-1, #f8f9fa); -} - -/* Pagination */ -.aksrt-forum-board__pagination { - display: flex; - justify-content: center; - padding: 20px 0; -} - -/* Empty state */ -.aksrt-forum-board__empty { - text-align: center; - padding: 48px 24px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-board__empty i { - font-size: 40px; - margin-bottom: 12px; - display: block; -} - -.aksrt-forum-board__empty a { - display: inline-block; - margin-top: 12px; - color: var(--color-primary); -} - - -/* Board sort tabs */ -.aksrt-forum-board__tabs { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 16px; - padding: 8px; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-board__tab { - display: inline-flex; - align-items: center; - padding: 8px 14px; - border-radius: 999px; - color: var(--color-ink-3, #5c6370); - text-decoration: none; - font-size: 14px; - font-weight: 600; - transition: background .15s ease, color .15s ease; -} - -.aksrt-forum-board__tab:hover { - background: rgba(240, 68, 148, 0.08); - color: var(--color-primary, #f04494); -} - -.aksrt-forum-board__tab.is-active { - background: var(--color-primary, #f04494); - color: #fff; -} - -.aksrt-forum-board__governance--footer { - margin-top: 28px; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 16px; -} - -.aksrt-forum-topic-row { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 14px 16px; - margin-bottom: 10px; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic-row__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; -} - -.aksrt-forum-topic-row__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-row__title-wrap { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic-row__title { - margin: 0; - font-size: 16px; - font-weight: 700; -} - -.aksrt-forum-topic-row__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-row__meta { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 6px; - color: var(--color-ink-4, #8a909c); - font-size: 12px; -} - -.aksrt-forum-topic-row__badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 999px; - font-size: 12px; - font-weight: 700; - line-height: 1.4; -} - -.aksrt-forum-topic-row__badge--sticky { background: rgba(255, 149, 0, 0.12); color: #c77700; } -.aksrt-forum-topic-row__badge--featured { background: rgba(240, 68, 148, 0.12); color: #f04494; } -.aksrt-forum-topic-row__badge--question { background: rgba(47, 128, 237, 0.12); color: #2f80ed; } -.aksrt-forum-topic-row__badge--question.is-solved { background: rgba(39, 174, 96, 0.12); color: #27ae60; } - -.aksrt-forum-topic-row__cover { - position: relative; - width: 84px; - height: 60px; - flex: 0 0 auto; - border-radius: 10px; - overflow: hidden; - background: var(--surface-2, #f5f6f8); -} - -.aksrt-forum-topic-row__cover img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.aksrt-forum-topic-row__cover .fa-play-circle, -.aksrt-forum-topic-row__cover-count { - position: absolute; - right: 6px; - bottom: 6px; - color: #fff; - text-shadow: 0 1px 2px rgba(0,0,0,.35); -} - -/* detail list card (zibll forum-posts detail) */ -.aksrt-forum-topic-row.detail, -.aksrt-forum-posts.detail { - display: block; - padding: 16px; - margin-bottom: 12px; - border-radius: 14px; - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm, 0 1px 4px rgba(0,0,0,.06)); -} -.aksrt-forum-topic-row__author-card { gap: 10px; } -.aksrt-forum-topic-row__author-name { font-weight: 700; font-size: 14px; } -.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover, -.aksrt-forum-posts.detail .forum-lists-cover { - display: block; - width: 100%; - height: auto; - max-height: 280px; - margin: 0 0 12px; - border-radius: 12px; - overflow: hidden; -} -.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover img { - width: 100%; - height: auto; - max-height: 280px; - object-fit: cover; - display: block; -} -.aksrt-forum-topic-row .excerpt { - margin-top: 6px; - font-size: 14px; - line-height: 1.65; - color: var(--color-ink-2, #4b5563); -} -.aksrt-forum-topic-row .lists-imgs { - display: grid; - gap: 6px; - margin-top: 8px; -} -.aksrt-forum-topic-row .lists-imgs.count-1 { grid-template-columns: 1fr; max-width: 280px; } -.aksrt-forum-topic-row .lists-imgs.count-2 { grid-template-columns: 1fr 1fr; } -.aksrt-forum-topic-row .lists-imgs.count-3, -.aksrt-forum-topic-row .lists-imgs.count-4 { grid-template-columns: 1fr 1fr; } -.aksrt-forum-topic-row .lists-imgs__item { - display: block; - border-radius: 10px; - overflow: hidden; - aspect-ratio: 1; - background: var(--surface-2, #f3f4f6); -} -.aksrt-forum-topic-row .lists-imgs__item img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} -.aksrt-forum-topic-row__vote { display: grid; gap: 6px; } -.aksrt-forum-topic-row__vote-opt { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(80px, 2fr) 42px; - gap: 8px; - align-items: center; - font-size: 13px; -} -.aksrt-forum-topic-row__vote-bar { - height: 8px; - border-radius: 999px; - background: var(--surface-4, #eef0f3); - overflow: hidden; -} -.aksrt-forum-topic-row__vote-bar i { - display: block; - height: 100%; - background: var(--color-primary, #f04494); -} -.aksrt-forum-topic-row__footer .but-plate, -.aksrt-forum-topic-row__board.but { - display: inline-flex; - align-items: center; - max-width: 46%; - padding: 4px 10px; - border-radius: 999px; - background: var(--surface-4, #f3f4f6); - color: var(--color-ink-2, #5c6370); - text-decoration: none; - font-size: 12px; - font-weight: 600; -} -.aksrt-forum-topic-row__actions { - display: inline-flex; - gap: 12px; - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} -.aksrt-forum-topic-row__actions a { color: inherit; text-decoration: none; } -.aksrt-forum-topic-row.mini { display: flex; gap: 12px; align-items: flex-start; } - -.aksrt-forum-board-card__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 12px; -} - -.aksrt-forum-board-card { - display: flex; - flex-direction: column; - overflow: hidden; - border-radius: var(--radius-xl, 12px); - background: var(--surface-1, #fff); - box-shadow: var(--shadow-sm); - text-decoration: none; - color: inherit; -} - -.aksrt-forum-board-card__cover { - display: block; - height: 96px; - background-size: cover; - background-position: center; -} - -.aksrt-forum-board-card__cover--empty { - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, rgba(240,68,148,.08), rgba(47,128,237,.08)); - color: var(--color-primary, #f04494); - font-size: 24px; -} - -.aksrt-forum-board-card__body { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px 14px 14px; -} - -.aksrt-forum-board-card__title { - font-weight: 700; -} - -.aksrt-forum-board-card__desc, -.aksrt-forum-board-card__meta { - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum__empty, -.aksrt-forum-board__empty, -.aksrt-forum-tag__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; - padding: 48px 20px; - text-align: center; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum__empty a, -.aksrt-forum-board__empty a { - color: var(--color-primary, #f04494); - font-weight: 600; - text-decoration: none; -} diff --git a/assets/css/frontend/forum/compose.css b/assets/css/frontend/forum/compose.css deleted file mode 100644 index 77710245..00000000 --- a/assets/css/frontend/forum/compose.css +++ /dev/null @@ -1,714 +0,0 @@ -/* ============================================================ - * Compose page - * ============================================================ */ - -.aksrt-forum-compose { - padding-top: 24px; - padding-bottom: 48px; - max-width: 1100px; - margin: 0 auto; -} - -.aksrt-forum-compose__header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; -} - -.aksrt-forum-compose__header h1 { - margin: 0; - font-size: 22px; -} - -.aksrt-forum-compose__notice { - margin: 8px 0 0; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-compose__back { - color: var(--color-ink-3, #8a909c); - text-decoration: none; - font-size: 14px; -} - -.aksrt-forum-compose__back:hover { - color: var(--color-primary); -} - -/* §7.4 — Two-column layout: main + sidebar */ -.aksrt-forum-compose__layout { - display: grid; - grid-template-columns: 1fr 311px; - gap: 24px; - align-items: start; -} - -.aksrt-forum-compose__main { - min-width: 0; -} - -.aksrt-forum-compose__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* §7.4 — Sidebar panels */ -.aksrt-forum-compose__panel { - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__panel-title { - margin: 0; - padding: 12px 16px; - font-size: 14px; - font-weight: 600; - color: var(--color-ink); - border-bottom: 1px solid var(--color-line, #eceff4); - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-compose__panel-title i { - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-compose__panel-body { - padding: 14px 16px; -} - -.aksrt-forum-compose__readonly { - margin: 0; - color: var(--color-ink-2, #555); - font-size: 14px; -} - -.aksrt-forum-compose__status-line { - margin: 0 0 12px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-compose__form { - padding: 0; - background: transparent; - border-radius: 0; - box-shadow: none; -} - -.aksrt-forum-compose__main { - padding: 24px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__field { - margin-bottom: 16px; -} - -.aksrt-forum-compose__field:last-child { - margin-bottom: 0; -} - -.aksrt-forum-compose__field label { - display: block; - font-size: 14px; - font-weight: 500; - color: var(--color-ink); - margin-bottom: 6px; -} - -.aksrt-forum-compose__field .required { - color: var(--color-error, #f5222d); -} - -.aksrt-forum-compose__field select, -.aksrt-forum-compose__field input[type="text"], -.aksrt-forum-compose__field input[type="number"], -.aksrt-forum-compose__field input[type="url"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 8px 12px; - font-size: 14px; - font-family: inherit; - box-sizing: border-box; -} - -.aksrt-forum-compose__field textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - resize: vertical; - font-family: inherit; - box-sizing: border-box; -} - -.aksrt-forum-compose__checkbox { - display: inline-flex; - align-items: center; - gap: 8px; - cursor: pointer; -} - -/* §7.4 — wp_editor wrapper */ -.aksrt-forum-compose__editor-wrap { - margin-bottom: 0; -} - -.aksrt-forum-compose__editor-wrap .wp-editor-wrap { - border-radius: 8px; - overflow: hidden; -} - -.aksrt-forum-compose__editor-wrap .wp-editor-container { - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; -} - -/* §7.4 — Character counter */ -.aksrt-forum-compose__char-counter { - display: block; - text-align: right; - font-size: 12px; - color: var(--color-ink-3, #8a909c); - margin-top: 4px; -} - -.aksrt-forum-compose__char-counter.is-near { - color: var(--color-warning, #d48806); -} - -.aksrt-forum-compose__char-counter.is-over { - color: var(--color-error, #cf1322); - font-weight: 600; -} - -/* §7.4 — Draft notice */ -.aksrt-forum-compose__draft-notice { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-bottom: 20px; - padding: 12px 16px; - background: var(--color-info-bg, #e6f7ff); - border: 1px solid var(--color-info-border, #91d5ff); - border-radius: 8px; - font-size: 14px; - color: var(--color-info, #0050b3); -} - -.aksrt-forum-compose__draft-toggle { - border: none; - background: none; - color: var(--color-primary); - cursor: pointer; - font-size: 13px; - display: inline-flex; - align-items: center; - gap: 4px; - padding: 0; -} - -.aksrt-forum-compose__draft-list { - width: 100%; - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 6px; -} - -.aksrt-forum-compose__draft-item { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: var(--surface-1, #fff); - border-radius: 6px; - font-size: 13px; -} - -.aksrt-forum-compose__draft-title { - flex: 1; - color: var(--color-ink); - text-decoration: none; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.aksrt-forum-compose__draft-title:hover { - color: var(--color-primary); -} - -.aksrt-forum-compose__draft-meta { - color: var(--color-ink-3, #8a909c); - font-size: 12px; - flex-shrink: 0; -} - -.aksrt-forum-compose__draft-delete { - border: none; - background: none; - color: var(--color-ink-3, #8a909c); - cursor: pointer; - padding: 2px; - font-size: 12px; -} - -.aksrt-forum-compose__draft-delete:hover { - color: var(--color-error, #cf1322); -} - -/* §7.4 — Remote search select */ -.aksrt-remote-select { - position: relative; -} - -.aksrt-remote-select__input { - width: 100%; -} - -.aksrt-remote-select__dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 100; - background: var(--surface-1, #fff); - border: none; - border-top: none; - border-radius: 0 0 12px 12px; - max-height: 200px; - overflow-y: auto; - box-shadow: var(--shadow-lg); -} - -.aksrt-remote-select__option { - padding: 8px 12px; - cursor: pointer; - font-size: 14px; -} - -.aksrt-remote-select__option:hover { - background: var(--color-primary-bg, #e6f7ff); -} - -/* §7.4 — Tag tokenizer */ -.aksrt-tag-input { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - padding: 8px 10px; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 12px; - background: var(--surface-4, #fff); - min-height: 42px; -} - -.aksrt-tag-input__tokens { - display: flex; - flex-wrap: wrap; - gap: 4px; -} - -.aksrt-tag-input__token { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - background: var(--color-primary-bg, #e6f7ff); - color: var(--color-primary, #1890ff); - border-radius: 4px; - font-size: 13px; -} - -.aksrt-tag-input__token-remove { - border: none; - background: none; - color: var(--color-primary, #1890ff); - cursor: pointer; - padding: 0; - font-size: 14px; - line-height: 1; - opacity: 0.7; -} - -.aksrt-tag-input__token-remove:hover { - opacity: 1; -} - -.aksrt-tag-input__input { - flex: 1; - min-width: 80px; - border: none !important; - padding: 2px 4px !important; - font-size: 13px; - outline: none; - box-shadow: none !important; -} - -.aksrt-tag-input__dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 100; - background: var(--surface-1, #fff); - border: none; - border-top: none; - border-radius: 0 0 12px 12px; - max-height: 160px; - overflow-y: auto; - box-shadow: var(--shadow-lg); -} - -.aksrt-tag-input__suggestion { - padding: 6px 12px; - cursor: pointer; - font-size: 13px; -} - -.aksrt-tag-input__suggestion:hover { - background: var(--color-primary-bg, #e6f7ff); -} - -.aksrt-forum-compose__vote-fields[hidden] { - display: none; -} - -.aksrt-forum-compose__vote { - margin-bottom: 20px; - padding: 18px; - border: none; - border-radius: var(--radius-2xl, 16px); - background: var(--surface-1, #f8f9fa); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__vote-head { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 12px; -} - -.aksrt-forum-compose__vote-head p { - margin: 0; - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-compose__feedback, -.aksrt-forum-board__application-feedback, -.aksrt-forum-topic__vote-feedback, -.aksrt-forum-topic__moderation-feedback { - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-compose__feedback.is-success, -.aksrt-forum-board__application-feedback.is-success, -.aksrt-forum-topic__vote-feedback.is-success, -.aksrt-forum-topic__moderation-feedback.is-success { - color: var(--color-success, #389e0d); -} - -.aksrt-forum-compose__feedback.is-error, -.aksrt-forum-board__application-feedback.is-error, -.aksrt-forum-topic__vote-feedback.is-error, -.aksrt-forum-topic__moderation-feedback.is-error { - color: var(--color-error, #cf1322); -} - -.aksrt-forum-compose__feedback.is-loading, -.aksrt-forum-board__application-feedback.is-loading, -.aksrt-forum-topic__vote-feedback.is-loading, -.aksrt-forum-topic__moderation-feedback.is-loading { - color: var(--color-primary); -} - -.aksrt-forum-compose__actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 12px; -} - -.aksrt-forum-compose__submit { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 24px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; - font-size: 15px; - font-weight: 500; -} - -.aksrt-forum-compose__submit:hover { - opacity: 0.85; -} - -.aksrt-forum-compose__submit:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.aksrt-forum-compose__draft { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 24px; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - background: var(--surface-1, #fff); - color: var(--color-ink-3, #8a909c); - cursor: pointer; - font-size: 15px; -} - -.aksrt-forum-compose__draft:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - background: var(--color-primary-softer, color-mix(in srgb, var(--color-primary) 6%, transparent)); -} - -/* ── topic cover (§7.3) ── */ - -.aksrt-forum-topic__cover { - margin-bottom: 20px; - border-radius: 12px; - overflow: hidden; - background: var(--surface-2, #f5f7fa); -} - -.aksrt-forum-topic__cover-img { - width: 100%; - display: block; - object-fit: cover; -} - -.aksrt-forum-topic__cover-slides { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; -} - -.aksrt-forum-topic__cover-slides img { - width: 100%; - aspect-ratio: 1; - object-fit: cover; -} - -.aksrt-forum-topic__cover-player { - position: relative; - cursor: pointer; -} - -.aksrt-forum-topic__cover-poster { - width: 100%; - display: block; - object-fit: cover; -} - -.aksrt-forum-topic__cover-play-btn { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border: none; - background: rgba(0, 0, 0, 0.5); - color: #fff; - font-size: 48px; - border-radius: 50%; - width: 72px; - height: 72px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: background 0.15s; -} - -.aksrt-forum-topic__cover-play-btn:hover { - background: rgba(0, 0, 0, 0.7); -} - -/* ── topic list cover thumbnails (§7.3) ── */ - -.aksrt-forum-topic-item__cover { - flex-shrink: 0; - width: 120px; - height: 80px; - border-radius: 8px; - overflow: hidden; - background: var(--surface-2, #f5f7fa); -} - -.aksrt-forum-topic-item__cover img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.aksrt-forum-topic-item__cover-video, -.aksrt-forum-topic-item__cover-slide { - position: relative; - width: 100%; - height: 100%; -} - -.aksrt-forum-topic-item__cover-video img, -.aksrt-forum-topic-item__cover-slide img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.aksrt-forum-topic-item__cover-video i { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 24px; - color: #fff; - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); -} - -.aksrt-forum-topic-item__cover-count { - position: absolute; - bottom: 4px; - right: 4px; - padding: 1px 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.55); - color: #fff; - font-size: 11px; -} - -/* ── compose cover upload (§7.3) ── */ - -.aksrt-forum-compose__cover-upload { - margin-bottom: 20px; -} - -.aksrt-forum-compose__cover-preview { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 10px; - min-height: 60px; -} - -.aksrt-forum-compose__cover-preview img { - width: 120px; - height: 80px; - object-fit: cover; - border-radius: 12px; - border: none; - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-compose__cover-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 18px; - border: 1px dashed var(--color-line, #d9d9d9); - border-radius: 12px; - background: var(--surface-4, #fff); - color: var(--color-ink-3, #8a909c); - cursor: pointer; - font-size: 14px; - transition: border-color 0.15s, color 0.15s, background 0.15s; -} - -.aksrt-forum-compose__cover-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - background: var(--color-primary-soft); -} - -.aksrt-forum-compose__field input[type="url"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - font-family: inherit; -} - -@media (max-width: 767px) { - .aksrt-forum-board__header, - .aksrt-forum-board__actions, - .aksrt-forum-topic__vote-head, - .aksrt-forum-topic__moderation-head, - .aksrt-forum-topic__vote-actions, - .aksrt-forum-topic__moderation-actions, - .aksrt-forum-compose__header, - .aksrt-forum-compose__actions { - flex-direction: column; - align-items: stretch; - } - - .aksrt-forum-topic-item { - flex-wrap: wrap; - } - - .aksrt-forum-topic-item__actions { - width: 100%; - flex-direction: row; - flex-wrap: wrap; - } - - .aksrt-forum-topic-item__cover { - width: 80px; - height: 56px; - } - - .aksrt-forum-topic__cover-slides { - grid-template-columns: repeat(2, 1fr); - } - - .aksrt-forum-topic__moderation-grid, - .aksrt-forum-compose__grid { - grid-template-columns: 1fr; - } - - /* §7.4 — Stack sidebar below main on mobile */ - .aksrt-forum-compose__layout { - grid-template-columns: 1fr; - } - - .aksrt-forum-compose__sidebar { - order: 2; - } - - .aksrt-forum-compose { - max-width: 100%; - } -} - diff --git a/assets/css/frontend/forum/extras.css b/assets/css/frontend/forum/extras.css deleted file mode 100644 index 527be719..00000000 --- a/assets/css/frontend/forum/extras.css +++ /dev/null @@ -1,7 +0,0 @@ -@import url("extras/base.css"); -@import url("extras/reply-header-tabs-filters.css"); -@import url("extras/load-more.css"); -@import url("extras/additional-role-badges.css"); -@import url("extras/like-button.css"); -@import url("extras/reply-action-buttons.css"); -@import url("extras/quick-post-bar.css"); diff --git a/assets/css/frontend/forum/extras/additional-role-badges.css b/assets/css/frontend/forum/extras/additional-role-badges.css deleted file mode 100644 index b02887c9..00000000 --- a/assets/css/frontend/forum/extras/additional-role-badges.css +++ /dev/null @@ -1,52 +0,0 @@ -/* --- additional role badges --- */ - -.aksrt-forum-reply__role-badge--author { - background: #fff7e6; - color: #d46b08; -} - -.aksrt-forum-reply__role-badge--super { - background: #f6ffed; - color: #389e0d; -} - -.aksrt-forum-reply__role-badge--catmod { - background: #fff0f6; - color: #c41d7f; -} - -/* --- hot comment (神评) badge --- */ - -.aksrt-forum-reply__badge { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 1px 6px; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - line-height: 1.5; -} - -.aksrt-forum-reply__badge--hot { - background: #fff2e8; - color: #d4380d; -} - -.aksrt-forum-reply__badge--pinned { - background: #e6f7ff; - color: #096dd9; -} - -.aksrt-forum-reply.is-hot { - border-left: 3px solid #fa541c; -} - -.aksrt-forum-reply.is-pinned { - border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent); -} - -.aksrt-forum-reply.is-pinned.is-hot { - border-left: 3px solid #fa541c; -} - diff --git a/assets/css/frontend/forum/extras/base.css b/assets/css/frontend/forum/extras/base.css deleted file mode 100644 index 645983f7..00000000 --- a/assets/css/frontend/forum/extras/base.css +++ /dev/null @@ -1,621 +0,0 @@ -/* ============================================================ - * Forum tag archive page - * ============================================================ */ - -.aksrt-forum-tag { - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-tag__header { - margin-bottom: 24px; - padding: 20px; - background: var(--surface-1, #fff); - border-radius: 12px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-forum-tag__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; -} - -.aksrt-forum-tag__title i { - color: var(--color-primary); -} - -.aksrt-forum-tag__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-tag__count { - margin: 0; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-tag__list { - display: flex; - flex-direction: column; - gap: 1px; -} - -.aksrt-forum-topic-card { - display: flex; - padding: 16px; - background: var(--surface-1, #fff); - transition: background 0.15s; -} - -.aksrt-forum-topic-card:first-child { - border-radius: 8px 8px 0 0; -} - -.aksrt-forum-topic-card:last-child { - border-radius: 0 0 8px 8px; -} - -.aksrt-forum-topic-card:hover { - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-topic-card__body { - flex: 1; - min-width: 0; -} - -.aksrt-forum-topic-card__title { - margin: 0 0 6px; - font-size: 16px; - font-weight: 500; -} - -.aksrt-forum-topic-card__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-card__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-card__excerpt { - margin: 0 0 8px; - font-size: 13px; - color: var(--color-ink-3, #8a909c); - line-height: 1.5; -} - -.aksrt-forum-topic-card__meta { - display: flex; - gap: 16px; - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic-card__meta i { - margin-right: 3px; -} - -.aksrt-forum-tag__pagination { - display: flex; - justify-content: center; - padding: 20px 0; -} - -.aksrt-forum-tag__empty { - text-align: center; - padding: 48px 24px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-tag__empty i { - font-size: 40px; - margin-bottom: 12px; - display: block; -} - -/* ============================================================ - * Forum widgets - * ============================================================ */ - -.aksrt-forum-widget__list { - margin: 0; - padding: 0; - list-style: none; -} - -.aksrt-forum-widget__item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 0; - border-bottom: 1px solid var(--color-line-soft, #f0f1f3); -} - -.aksrt-forum-widget__item:last-child { - border-bottom: none; -} - -.aksrt-forum-widget__rank { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: var(--surface-2, #f4f5f7); - color: var(--color-ink-3, #8a909c); - font-size: 12px; - font-weight: 600; - flex-shrink: 0; -} - -.aksrt-forum-widget__item:nth-child(1) .aksrt-forum-widget__rank { - background: var(--color-error, #ef4444); - color: #fff; -} - -.aksrt-forum-widget__item:nth-child(2) .aksrt-forum-widget__rank { - background: var(--color-warning, #f59e0b); - color: #fff; -} - -.aksrt-forum-widget__item:nth-child(3) .aksrt-forum-widget__rank { - background: var(--color-info, #3b82f6); - color: #fff; -} - -.aksrt-forum-widget__link { - flex: 1; - min-width: 0; - color: var(--color-ink); - text-decoration: none; - font-size: 14px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.aksrt-forum-widget__link:hover { - color: var(--color-primary); -} - -.aksrt-forum-widget__views, -.aksrt-forum-widget__author { - font-size: 12px; - color: var(--color-ink-4, #8a909c); - white-space: nowrap; -} - -.aksrt-forum-widget__boards { - margin: 0; - padding: 0; - list-style: none; -} - -.aksrt-forum-widget__board { - padding: 8px 0; - border-bottom: 1px solid var(--color-line-soft, #f0f1f3); -} - -.aksrt-forum-widget__board:last-child { - border-bottom: none; -} - -.aksrt-forum-widget__board a { - display: flex; - align-items: center; - justify-content: space-between; - color: var(--color-ink); - text-decoration: none; - font-size: 14px; -} - -.aksrt-forum-widget__board a:hover { - color: var(--color-primary); -} - -.aksrt-forum-widget__board a i { - margin-right: 6px; - color: var(--color-primary); -} - -.aksrt-forum-widget__count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 20px; - padding: 0 6px; - background: var(--surface-2, #f4f5f7); - border-radius: var(--radius-pill, 999px); - color: var(--color-ink-3, #8a909c); - font-size: 11px; - font-weight: 600; -} - -.aksrt-forum-widget__empty { - text-align: center; - padding: 16px; - color: var(--color-ink-4, #8a909c); - font-size: 13px; -} - -/* ── bbs_type / question badges (§7.3) ── */ - -.aksrt-forum-topic__badge--question { - background: #e6f7ff; - color: var(--color-primary); -} - -.aksrt-forum-topic__badge--solved { - background: #f6ffed; - color: #52c41a; -} - -.aksrt-forum-topic-item__badge--question { - background: #e6f7ff; - color: var(--color-primary); -} - -.aksrt-forum-topic-item__badge--solved { - background: #f6ffed; - color: #52c41a; -} - -/* ── favorite button (§7.3) ── */ - -.js-aksrt-topic-favorite { - background: none; - border: none; - cursor: pointer; - transition: color .15s; -} - -.js-aksrt-topic-favorite[data-favorited="1"] { - color: var(--color-warning, #faad14); -} - -.js-aksrt-topic-favorite:disabled { - opacity: .5; - cursor: not-allowed; -} - -/* ── score actions (§7.3) ── */ - -.aksrt-forum-topic__score-actions { - display: flex; - align-items: center; - gap: 10px; - margin: 16px 0; - padding: 12px 16px; - background: var(--surface-1, #fff); - border: 1px solid var(--border-2, #e8e8e8); - border-radius: var(--radius-m, 8px); -} - -.aksrt-forum-topic__score-total { - font-size: 14px; - font-weight: 600; - color: var(--color-ink, #333); -} - -.aksrt-forum-topic__score-total i { - color: var(--color-warning, #faad14); - margin-right: 4px; -} - -.aksrt-forum-topic__score-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: 1px solid var(--border-2, #e0e0e0); - border-radius: var(--radius-s, 4px); - background: var(--surface-1, #fff); - cursor: pointer; - font-size: 12px; - transition: all .15s; -} - -.aksrt-forum-topic__score-btn:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - -.aksrt-forum-topic__score-btn--up:hover:not(:disabled) { - color: var(--color-success, #52c41a); - border-color: var(--color-success, #52c41a); -} - -.aksrt-forum-topic__score-btn--down:hover:not(:disabled) { - color: var(--color-error, #f5222d); - border-color: var(--color-error, #f5222d); -} - -.aksrt-forum-topic__score-btn:disabled { - opacity: .4; - cursor: not-allowed; -} - -.aksrt-forum-topic__score-badge { - color: var(--color-warning, #faad14); - font-weight: 600; -} - -.aksrt-forum-topic__score-feedback { - font-size: 13px; - color: var(--color-ink-4, #999); -} - -/* ── vote enhancements (§7.3) ── */ - -.aksrt-forum-topic__vote--pk .aksrt-forum-topic__vote-options { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -.aksrt-forum-topic__vote--expired { - opacity: .7; -} - -.aksrt-forum-topic__vote-type-badge { - display: inline-block; - padding: 1px 6px; - background: var(--color-primary); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 700; - margin-left: 6px; -} - -.aksrt-forum-topic__vote-expired-badge { - display: inline-block; - padding: 1px 6px; - background: var(--color-error, #f5222d); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - margin-left: 6px; -} - -.aksrt-forum-topic__vote-expiry-info { - color: var(--color-ink-4, #999); - font-size: 12px; - margin-left: 6px; -} - -/* ── topic_cat chip (§7.3) ── */ - -.aksrt-forum-topic__chip--topic-cat { - background: var(--color-primary-bg, #e6f7ff); - color: var(--color-primary); - font-weight: 600; -} - -/* ── report modal (§7.5) ── */ - -.aksrt-forum-topic__header-top { - display: flex; - flex-direction: column; - gap: 12px; -} - -.aksrt-forum-report-modal { - position: fixed; - inset: 0; - z-index: 9999; -} - -.aksrt-forum-report-modal__backdrop { - position: absolute; - inset: 0; - background: rgba(15, 23, 42, 0.55); -} - -.aksrt-forum-report-modal__dialog { - position: relative; - max-width: 520px; - margin: 8vh auto 0; - padding: 20px; - border-radius: 14px; - background: var(--surface-1, #fff); - box-shadow: 0 18px 60px rgba(15, 23, 42, 0.2); - z-index: 1; -} - -.aksrt-forum-report-modal__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} - -.aksrt-forum-report-modal__head h2 { - margin: 0; - font-size: 18px; -} - -.aksrt-forum-report-modal__close { - border: none; - background: transparent; - font-size: 24px; - line-height: 1; - cursor: pointer; - color: var(--color-ink-3, #64748b); -} - -.aksrt-forum-report-modal__target { - margin: 0 0 14px; - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-report-modal__actions { - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-forum-report-modal__feedback { - font-size: 13px; -} - -.is-report-modal-open { - overflow: hidden; -} - -.aksrt-forum-reply__report-btn { - display: inline-flex; - align-items: center; - gap: 4px; - border: none; - background: transparent; - color: var(--color-ink-4, #94a3b8); - cursor: pointer; - padding: 0; - font-size: 12px; -} - -.aksrt-forum-reply__report-btn:hover { - color: var(--color-error, #ef4444); -} - -/* ── moderation enhancements (§7.5) ── */ - -.aksrt-forum-topic__moderation-note { - margin-bottom: 16px; - padding: 12px 14px; - border-radius: 10px; - background: var(--surface-2, #f8fafc); - color: var(--color-ink-3, #64748b); - font-size: 13px; -} - -.aksrt-forum-topic__moderation-note strong { - color: var(--color-ink, #334155); - margin-right: 6px; -} - -.aksrt-forum-topic__field small { - display: block; - margin-top: 6px; - color: var(--color-ink-4, #94a3b8); - font-size: 12px; -} - -/* ── paywall / credits unlock (§7.3) ── */ - -.aksrt-forum-topic__pay-price { - display: inline-block; - margin-top: 12px; - padding: 6px 16px; - background: #fff7e6; - color: #d46b08; - border-radius: 8px; - font-size: 15px; - font-weight: 600; -} - -.aksrt-forum-topic__unlock-feedback { - display: block; - margin-top: 10px; - font-size: 13px; -} - -.aksrt-forum-compose__visibility-extra select[multiple] { - min-height: 90px; -} - -.aksrt-forum-compose__visibility-extra input[type="number"] { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-size: 14px; - font-family: inherit; -} - -/* ── adopted answer (§7.3) ── */ - -.aksrt-forum-reply.is-adopted { - border: 2px solid var(--color-success, #52c41a); - border-radius: var(--radius-m, 8px); - padding: 12px; - background: #f6ffed; -} - -.aksrt-forum-reply__adopted-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - background: var(--color-success, #52c41a); - color: #fff; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; -} - -.aksrt-forum-reply__adopt-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border: 1px solid var(--color-success, #52c41a); - border-radius: var(--radius-s, 4px); - background: transparent; - color: var(--color-success, #52c41a); - font-size: 12px; - cursor: pointer; - transition: all .15s; -} - -.aksrt-forum-reply__adopt-btn:hover:not(:disabled) { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-reply__adopt-btn:disabled, -.aksrt-forum-reply__adopt-btn.is-adopted { - opacity: .6; - cursor: not-allowed; -} - -/* ── moderator role badge (§7.5) ── */ - -.aksrt-forum-reply__role-badge { - display: inline-flex; - align-items: center; - padding: 1px 6px; - border-radius: var(--radius-pill, 999px); - font-size: 11px; - font-weight: 600; - line-height: 1.5; -} - -.aksrt-forum-reply__role-badge--admin { - background: #fff1f0; - color: #cf1322; -} - -.aksrt-forum-reply__role-badge--mod { - background: #e6f7ff; - color: #096dd9; -} - -/* ── §7.6 forum comment engine ── */ - diff --git a/assets/css/frontend/forum/extras/like-button.css b/assets/css/frontend/forum/extras/like-button.css deleted file mode 100644 index fd7099ce..00000000 --- a/assets/css/frontend/forum/extras/like-button.css +++ /dev/null @@ -1,62 +0,0 @@ -/* --- like button --- */ - -.aksrt-forum-reply__like-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border: none; - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: color .2s; -} - -.aksrt-forum-reply__like-btn:hover { - color: var(--color-primary); -} - -.aksrt-forum-reply__like-btn.is-active { - color: var(--color-primary); -} - -.aksrt-forum-reply__like-count { - font-size: 12px; - min-width: 10px; -} - -/* --- nested reply (楼中楼) --- */ - -.aksrt-forum-reply--nested { - padding-left: 0; - margin-left: 0; -} - -.aksrt-forum-reply__children { - margin-top: 8px; - padding: 8px 0 0 12px; - border-left: 2px solid var(--c-border, #f0f0f0); -} - -.aksrt-forum-reply__children .aksrt-forum-reply { - padding: 8px 0; - margin-bottom: 0; - border-bottom: 1px solid var(--c-border-light, #f5f5f5); -} - -.aksrt-forum-reply__children .aksrt-forum-reply:last-child { - border-bottom: none; -} - -.aksrt-forum-reply__avatar--sm img { - width: 28px; - height: 28px; - border-radius: 50%; -} - -.aksrt-forum-reply__reply-to { - font-size: 12px; - color: var(--c-text-3, #999); -} - diff --git a/assets/css/frontend/forum/extras/load-more.css b/assets/css/frontend/forum/extras/load-more.css deleted file mode 100644 index 9a201bc9..00000000 --- a/assets/css/frontend/forum/extras/load-more.css +++ /dev/null @@ -1,23 +0,0 @@ -/* --- load more --- */ - -.aksrt-forum-replies__load-more { - text-align: center; - padding: 16px 0; -} - -.aksrt-forum-replies__more-btn { - padding: 8px 24px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius, 6px); - background: none; - color: var(--c-text-2, #666); - font-size: 14px; - cursor: pointer; - transition: all .2s; -} - -.aksrt-forum-replies__more-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - diff --git a/assets/css/frontend/forum/extras/quick-post-bar.css b/assets/css/frontend/forum/extras/quick-post-bar.css deleted file mode 100644 index 8e151fe1..00000000 --- a/assets/css/frontend/forum/extras/quick-post-bar.css +++ /dev/null @@ -1,31 +0,0 @@ -/* Quick post bar */ - -.aksrt-forum-quick-post { - margin-bottom: 16px; -} - -.aksrt-forum-quick-post__input { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 16px; - border: 1px solid var(--c-border, #e8e8e8); - border-radius: var(--radius, 6px); - background: var(--c-fill-1, #fafafa); - color: var(--c-text-3, #999); - font-size: 14px; - text-decoration: none; - transition: border-color .2s, box-shadow .2s; -} - -.aksrt-forum-quick-post__input:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-forum-quick-post__input img { - width: 32px; - height: 32px; - border-radius: 50%; -} - diff --git a/assets/css/frontend/forum/extras/reply-action-buttons.css b/assets/css/frontend/forum/extras/reply-action-buttons.css deleted file mode 100644 index 7118ef22..00000000 --- a/assets/css/frontend/forum/extras/reply-action-buttons.css +++ /dev/null @@ -1,110 +0,0 @@ -/* --- reply action buttons --- */ - -.aksrt-forum-reply__reply-btn, -.aksrt-forum-reply__top-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border: none; - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: color .2s; -} - -.aksrt-forum-reply__reply-btn:hover, -.aksrt-forum-reply__top-btn:hover { - color: var(--color-primary); -} - -/* ── §7.3 Hidden content block [aksrt_hide] ── */ - -.aksrt-forum-hide { - margin: 16px 0; -} - -.aksrt-forum-hide--locked { - padding: 16px; - border: 1px dashed var(--c-border, #d9d9d9); - border-radius: var(--radius, 6px); - background: var(--c-fill-1, #fafafa); - text-align: center; -} - -.aksrt-forum-hide__message { - font-size: 13px; - color: var(--c-text-3, #999); -} - -.aksrt-forum-hide__message i { - margin-right: 4px; - color: var(--c-warning, #faad14); -} - -.aksrt-forum-hide--revealed { - padding: 12px 16px; - border-left: 3px solid var(--c-success, #52c41a); - background: var(--c-fill-1, #f6ffed); - border-radius: 0 var(--radius, 6px) var(--radius, 6px) 0; -} - -/* ── §7.10 Forum home tab engine ── */ - -.aksrt-forum-home-tabs { - display: flex; - gap: 4px; - padding: 0 0 12px; - margin-bottom: 16px; - border-bottom: 1px solid var(--c-border, #e8e8e8); - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.aksrt-forum-home-tabs__btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 6px 16px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius-pill, 999px); - background: none; - color: var(--c-text-3, #999); - font-size: 14px; - font-weight: 500; - cursor: pointer; - white-space: nowrap; - transition: all .2s; -} - -.aksrt-forum-home-tabs__btn:hover { - color: var(--c-text-1, #333); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-home-tabs__btn.is-active { - background: var(--color-primary); - color: #fff; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-home-tabs__badge { - display: inline-block; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: 9px; - background: #ff4d4f; - color: #fff; - font-size: 11px; - font-weight: 700; - line-height: 18px; - text-align: center; -} - -.aksrt-forum-home__load-more { - text-align: center; - padding: 16px 0; -} - diff --git a/assets/css/frontend/forum/extras/reply-header-tabs-filters.css b/assets/css/frontend/forum/extras/reply-header-tabs-filters.css deleted file mode 100644 index 5bb6e20f..00000000 --- a/assets/css/frontend/forum/extras/reply-header-tabs-filters.css +++ /dev/null @@ -1,69 +0,0 @@ -/* --- reply header tabs & filters --- */ - -.aksrt-forum-replies__header { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid var(--c-border, #e8e8e8); -} - -.aksrt-forum-replies__tabs { - display: flex; - align-items: center; - gap: 4px; -} - -.aksrt-forum-replies__tab { - padding: 4px 12px; - border: 1px solid var(--c-border, #d9d9d9); - border-radius: var(--radius-pill, 999px); - background: none; - color: var(--c-text-3, #999); - font-size: 13px; - cursor: pointer; - transition: all .2s; -} - -.aksrt-forum-replies__tab:hover { - color: var(--c-text-1, #333); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-replies__tab.is-active { - background: var(--color-primary); - color: #fff; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-forum-replies__filter { - display: flex; - align-items: center; - gap: 4px; - font-size: 13px; - color: var(--c-text-3, #999); - cursor: pointer; - margin-left: 8px; -} - -.aksrt-forum-replies__filter input[type="checkbox"] { - width: 14px; - height: 14px; -} - -.aksrt-forum-replies__empty { - padding: 40px 0; - text-align: center; - color: var(--c-text-3, #bbb); - font-size: 14px; -} - -.aksrt-forum-replies__empty i { - font-size: 24px; - display: block; - margin-bottom: 8px; -} - diff --git a/assets/css/frontend/forum/topic.css b/assets/css/frontend/forum/topic.css deleted file mode 100644 index 82bb2fdd..00000000 --- a/assets/css/frontend/forum/topic.css +++ /dev/null @@ -1,402 +0,0 @@ -/* ============================================================ - * Topic detail page - * ============================================================ */ - -.aksrt-forum-topic { - display: grid; - grid-template-columns: minmax(0, 1fr) 280px; - gap: 24px; - padding-top: 24px; - padding-bottom: 48px; -} - -.aksrt-forum-topic__shell { - min-width: 0; -} - -.aksrt-forum-topic__sidebar { - display: flex; - flex-direction: column; - gap: 16px; -} - -.aksrt-forum-topic__sidebar-card { - padding: 18px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__sidebar-card h2 { - margin: 0 0 12px; - font-size: 15px; -} - -.aksrt-forum-topic__sidebar-link { - display: flex; - flex-direction: column; - gap: 6px; - color: inherit; - text-decoration: none; -} - -.aksrt-forum-topic__sidebar-link span { - font-size: 13px; - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic__sidebar-author { - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-forum-topic__sidebar-author img { - border-radius: 50%; -} - -.aksrt-forum-topic__sidebar-author strong, -.aksrt-forum-topic__board-link { - color: var(--color-ink); -} - -.aksrt-forum-topic__board-link-wrap { - display: inline-flex; - align-items: center; - gap: 6px; -} - - -.aksrt-forum-topic__breadcrumb { - font-size: 13px; - color: var(--color-ink-4, #8a909c); - margin-bottom: 16px; -} - -.aksrt-forum-topic__breadcrumb a { - color: var(--color-primary); - text-decoration: none; -} - -.aksrt-forum-topic__sep { - margin: 0 6px; -} - -.aksrt-forum-topic__header { - margin-bottom: 20px; -} - -.aksrt-forum-topic__title { - margin: 0 0 8px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.aksrt-forum-topic__badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; -} - -.aksrt-forum-topic__badge--sticky { - background: var(--color-primary); - color: #fff; -} - -.aksrt-forum-topic__badge--sticky-super { - background: var(--focus-color, #fd53a1); - color: #fff; -} - -.aksrt-forum-topic__badge--sticky-global { - background: #e53e3e; - color: #fff; -} - -.aksrt-forum-topic__badge--featured { - background: var(--color-success, #52c41a); - color: #fff; -} - -.aksrt-forum-topic__badge--locked { - background: #fff7e6; - color: #d46b08; -} - -.aksrt-forum-topic__badge--visibility { - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); -} - -.aksrt-forum-topic__meta { - display: flex; - flex-wrap: wrap; - gap: 16px; - font-size: 13px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__author { - display: flex; - align-items: center; - gap: 6px; -} - -.aksrt-forum-topic__author img { - border-radius: 50%; -} - -.aksrt-forum-topic__quick-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 14px; -} - -.aksrt-forum-topic__quick-action, -.aksrt-forum-topic__primary-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 16px; - border: none; - border-radius: 999px; - background: var(--surface-1, #fff); - color: var(--color-ink); - text-decoration: none; - cursor: pointer; -} - -.aksrt-forum-topic__primary-btn { - background: var(--color-primary); - color: #fff; -} - -.aksrt-forum-topic__taxonomy { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 20px; -} - -.aksrt-forum-topic__chip { - display: inline-flex; - align-items: center; - padding: 6px 10px; - border-radius: 999px; - background: var(--surface-2, #f5f7fa); - color: var(--color-ink-3, #8a909c); - text-decoration: none; - font-size: 13px; -} - -.aksrt-forum-topic__chip--special { - background: #fff1f0; - color: #cf1322; -} - -.aksrt-forum-topic__content { - padding: 22px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); - margin-bottom: 24px; - line-height: 1.8; - font-size: 15px; -} - -.aksrt-forum-topic__content img { - max-width: 100%; - border-radius: 8px; -} - -.aksrt-forum-topic__vote, -.aksrt-forum-topic__moderation { - margin-bottom: 24px; - padding: 20px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__vote-head, -.aksrt-forum-topic__moderation-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 16px; -} - -.aksrt-forum-topic__vote-head h2, -.aksrt-forum-topic__moderation-head h2 { - margin: 0; - font-size: 18px; -} - -.aksrt-forum-topic__vote-head span, -.aksrt-forum-topic__moderation-head p { - color: var(--color-ink-3, #8a909c); - font-size: 13px; -} - -.aksrt-forum-topic__vote-options { - display: grid; - gap: 10px; -} - -.aksrt-forum-topic__vote-option { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 10px; - padding: 12px 14px; - border-radius: 10px; - background: var(--surface-1, #f8f9fa); -} - -.aksrt-forum-topic__vote-option.is-selected { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 12%, transparent)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-forum-topic__vote-count { - font-size: 12px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__vote-actions, -.aksrt-forum-topic__moderation-actions { - display: flex; - align-items: center; - gap: 12px; - margin-top: 16px; -} - -.aksrt-forum-topic__moderation-grid, -.aksrt-forum-compose__grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; -} - -.aksrt-forum-topic__field select, -.aksrt-forum-topic__field textarea, -.aksrt-forum-compose__field input[type="text"], -.aksrt-forum-compose__field textarea, -.aksrt-forum-compose__field select { - width: 100%; -} - -.aksrt-forum-topic__field label { - display: block; - margin-bottom: 6px; - font-size: 14px; - font-weight: 500; -} - -.aksrt-forum-topic__field select, -.aksrt-forum-topic__field textarea { - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 10px 12px; - font-family: inherit; - font-size: 14px; -} - -.aksrt-forum-topic__denied { - padding: 56px 24px; - text-align: center; - background: var(--surface-1, #fff); - border-radius: 12px; -} - -.aksrt-forum-topic__denied i { - font-size: 42px; - margin-bottom: 14px; - color: var(--color-ink-4, #8a909c); -} - -.aksrt-forum-topic__replies { - margin-bottom: 24px; -} - -.aksrt-forum-topic__replies-title { - font-size: 16px; - font-weight: 600; - margin-bottom: 16px; - color: var(--color-ink); -} - -.aksrt-forum-topic__reply-form { - padding: 20px; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic__reply-form h2, -.aksrt-forum-topic__reply-form h3 { - margin: 0 0 12px; - font-size: 15px; - color: var(--color-ink); -} - -.aksrt-forum-topic__reply-field textarea { - width: 100%; - border: 1px solid var(--color-line, #d9d9d9); - border-radius: 8px; - padding: 12px; - font-size: 14px; - resize: vertical; - font-family: inherit; -} - -.aksrt-forum-topic__reply-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 20px; - border: none; - border-radius: 8px; - background: var(--color-primary); - color: #fff; - cursor: pointer; - font-size: 14px; -} - -.aksrt-forum-topic__reply-btn:hover { - opacity: 0.85; -} - -.aksrt-forum-topic__locked-notice, -.aksrt-forum-topic__login-notice { - text-align: center; - padding: 20px; - color: var(--color-ink-3, #8a909c); - font-size: 14px; -} - -@media (max-width: 960px) { - .aksrt-forum-topic { - grid-template-columns: minmax(0, 1fr); - } -} - -.aksrt-forum-topic__login-notice a { - color: var(--color-primary); -} - diff --git a/assets/css/frontend/pages/board-cards.css b/assets/css/frontend/pages/board-cards.css deleted file mode 100644 index 93fc53c6..00000000 --- a/assets/css/frontend/pages/board-cards.css +++ /dev/null @@ -1,58 +0,0 @@ -/* Board cards */ -.aksrt-forum-boards__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 12px; - margin-bottom: 32px; -} - -.aksrt-forum-board-card { - display: flex; - gap: 12px; - padding: 16px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line, #e8e8e8); - border-radius: 8px; - transition: box-shadow 0.2s; -} - -.aksrt-forum-board-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); -} - -.aksrt-forum-board-card__icon { - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: var(--color-primary-soft, #e6f4ff); - color: var(--color-primary); - font-size: 20px; - flex-shrink: 0; -} - -.aksrt-forum-board-card__title { - margin: 0 0 4px; - font-size: 14px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-forum-board-card__desc { - margin: 0 0 8px; - font-size: 12px; - color: var(--color-ink-3, #999); - line-height: 1.5; -} - -.aksrt-forum-board-card__meta { - font-size: 11px; - color: var(--color-ink-3, #999); -} - -.aksrt-forum-board-card__meta i { - margin-right: 3px; -} - diff --git a/assets/css/frontend/pages/topic-rows.css b/assets/css/frontend/pages/topic-rows.css deleted file mode 100644 index 6aeafc37..00000000 --- a/assets/css/frontend/pages/topic-rows.css +++ /dev/null @@ -1,56 +0,0 @@ -/* Topic rows */ -.aksrt-forum-topics__list { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 32px; -} - -.aksrt-forum-topic-row { - display: flex; - gap: 12px; - padding: 12px; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line, #e8e8e8); - border-radius: 8px; - transition: border-color 0.2s; -} - -.aksrt-forum-topic-row:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: var(--shadow-sm); -} - -.aksrt-forum-topic-row__avatar img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; -} - -.aksrt-forum-topic-row__title { - margin: 0 0 4px; - font-size: 14px; - font-weight: 600; -} - -.aksrt-forum-topic-row__title a { - color: var(--color-ink); - text-decoration: none; -} - -.aksrt-forum-topic-row__title a:hover { - color: var(--color-primary); -} - -.aksrt-forum-topic-row__meta { - display: flex; - gap: 12px; - font-size: 11px; - color: var(--color-ink-3, #999); -} - -.aksrt-forum-topic-row__meta i { - margin-right: 3px; -} - diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js index 5fea8c08..29b7cf84 100644 --- a/assets/js/cms-download.js +++ b/assets/js/cms-download.js @@ -367,8 +367,6 @@ } function payApiBase(apiKind) { - if (apiKind === 'topic') return '/pay/topic/'; - if (apiKind === 'plate_follow') return '/pay/plate-follow/'; return '/pay/content/'; } @@ -448,34 +446,15 @@ window.location.reload(); return; } - // Content free: reload. Topic/plate free: auto free-order POST (VIP free / free-after-coupon). - if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') { + // Content free: reload. + if (preview.is_free) { window.location.reload(); return; } const isPoints = preview.pay_modo === 'points'; const priceType = (preview._price_type || preview.price_type || ''); - const apiKind = preview._api_kind - || (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content')); - - // Auto-submit free topic/plate orders so user doesn't click through empty cash UI. - if (preview.is_free && (apiKind === 'topic' || apiKind === 'plate_follow') && !isPoints) { - open( - '

' + tt('confirm_buy', '确认购买') + '

' + - '

' + tt('processing', '处理中…') + '

', - returnFocusEl - ); - const freeBtn = document.createElement('button'); - freeBtn.type = 'button'; - freeBtn.style.display = 'none'; - freeBtn.setAttribute('data-price-type', priceType || ''); - document.body.appendChild(freeBtn); - const freeCode = String(preview.coupon_code || preview._coupon_code || ''); - submitPurchase(preview.post_id, 'cash', 'balance', freeBtn, returnFocusEl, freeCode, apiKind); - try { freeBtn.remove(); } catch (e0) { /* ignore */ } - return; - } + const apiKind = preview._api_kind || 'content'; let amountLabel = escapeHtml(preview.amount_label || ''); let baseLabel = escapeHtml(preview.base_label || ''); @@ -697,22 +676,6 @@ return; } - const topicBuyBtn = e.target.closest('[data-aksrt-pay-topic]'); - if (topicBuyBtn) { - e.preventDefault(); - const topicId = parseInt(topicBuyBtn.getAttribute('data-aksrt-pay-topic') || '0', 10); - if (topicId) openCashier(topicId, topicBuyBtn, '', '', 'topic'); - return; - } - - const plateFollowBuyBtn = e.target.closest('[data-aksrt-pay-plate-follow]'); - if (plateFollowBuyBtn) { - e.preventDefault(); - const plateId = parseInt(plateFollowBuyBtn.getAttribute('data-aksrt-pay-plate-follow') || '0', 10); - if (plateId) openCashier(plateId, plateFollowBuyBtn, '', '', 'plate_follow'); - return; - } - const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]'); if (buyBtn) { e.preventDefault(); @@ -743,7 +706,7 @@ requestDownload(btn); }); - // Bridge for forum-governance plate money cashier. + // Public cashier bridge (content pay). window.aksrtOpenCashier = function (postId, returnFocusEl, priceType, couponCode, apiKind) { openCashier(postId, returnFocusEl || null, priceType || '', couponCode || '', apiKind || 'content'); }; diff --git a/assets/js/core/ajax-list.js b/assets/js/core/ajax-list.js index 5d9f9a1e..ec651911 100644 --- a/assets/js/core/ajax-list.js +++ b/assets/js/core/ajax-list.js @@ -248,7 +248,7 @@ closestPager(link); if (!container) { // Fall back: parent stream list. - container = link.closest(".aksrt-product-grid, .aksrt-forum-topics__list, [data-aksrt-ajax-list]"); + container = link.closest("[data-aksrt-ajax-list]"); } if (!container) return Promise.resolve(); diff --git a/assets/js/forum-edit.js b/assets/js/forum-edit.js deleted file mode 100644 index 2cf0043b..00000000 --- a/assets/js/forum-edit.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * AKSRT Forum Edit — 帖子编辑页交互 - * - * 处理论坛帖子编辑页的表单增强和标签输入。 - * - * @package AKSRT - */ - -(function () { - 'use strict'; - - var form = document.querySelector('.aksrt-forum-edit__form'); - if (!form) return; - - // 标签输入增强:逗号分隔转标签 chips - var tagInput = form.querySelector('#topic_tags'); - if (tagInput) { - tagInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - } - }); - } - - // 表单提交确认 - form.addEventListener('submit', function (e) { - var title = form.querySelector('#topic_title'); - var content = form.querySelector('#topic_content'); - - if (title && title.value.trim() === '') { - e.preventDefault(); - alert('标题不能为空'); - title.focus(); - return; - } - - // TinyMCE 检查 - if (typeof tinyMCE !== 'undefined' && tinyMCE.get('topic_content')) { - var editorContent = tinyMCE.get('topic_content').getContent().trim(); - if (editorContent === '') { - e.preventDefault(); - alert('内容不能为空'); - return; - } - } - }); -})(); diff --git a/assets/js/forum-governance.js b/assets/js/forum-governance.js deleted file mode 100644 index ea89be4f..00000000 --- a/assets/js/forum-governance.js +++ /dev/null @@ -1,1623 +0,0 @@ -(function () { - 'use strict'; - - var rootData = window.aksrtData || {}; - var restRoot = (function () { - var siteUrl = String(rootData.siteUrl || '/').replace(/\/+$/, ''); - return siteUrl + '/wp-json/aksrt/v1'; - })(); - - function request(path, options) { - var config = options || {}; - var headers = config.headers || {}; - - return fetch(restRoot + path, { - method: config.method || 'GET', - headers: Object.assign({ - 'Content-Type': 'application/json', - 'X-WP-Nonce': rootData.restNonce || '', - }, headers), - body: config.body || null, - credentials: 'same-origin', - }).then(function (response) { - return response.json().catch(function () { - return {}; - }).then(function (payload) { - if (!response.ok || payload.success === false) { - throw new Error(payload.message || ('HTTP ' + response.status)); - } - return payload; - }); - }); - } - - function setFeedback(node, text, state) { - if (!node) { - return; - } - - node.textContent = text || ''; - node.classList.remove('is-success', 'is-error', 'is-loading'); - if (state) { - node.classList.add(state); - } - } - - function bindComposeToolbar(form) { - // §7.4: toolbar replaced by wp_editor TinyMCE — no-op stub kept for boot() compat - } - - function insertTextAtCursor(textarea, text, after, start, end) { - var value = textarea.value; - textarea.value = value.substring(0, start) + text + (after || '') + value.substring(end); - textarea.focus(); - } - - function askUrlInline(anchor, placeholder, callback) { - // kept for potential quicktags fallback - var existing = document.querySelector('.aksrt-inline-url-input'); - if (existing) existing.remove(); - - var input = document.createElement('input'); - input.type = 'url'; - input.className = 'aksrt-inline-url-input'; - input.placeholder = placeholder || 'https://'; - input.value = 'https://'; - input.style.cssText = 'display:block;width:100%;max-width:320px;margin:4px 0;padding:6px 10px;border:1px solid #dce3ec;border-radius:8px;font-size:13px;'; - - var parent = anchor.parentElement; - parent.insertBefore(input, anchor.nextSibling); - input.focus(); - input.select(); - - function done() { - var val = input.value.trim(); - input.remove(); - callback(val); - } - - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { e.preventDefault(); done(); } - if (e.key === 'Escape') { input.remove(); callback(''); } - }); - input.addEventListener('blur', function () { done(); }); - } - - function bindComposeVoteToggle(form) { - var toggle = form.querySelector('#compose-vote-enabled'); - var fields = form.querySelector('.aksrt-forum-compose__vote-fields'); - if (!toggle || !fields) { - return; - } - - function sync() { - fields.hidden = !toggle.checked; - } - - toggle.addEventListener('change', sync); - sync(); - } - - function getEditorContent() { - if (typeof tinymce !== 'undefined') { - var editor = tinymce.get('compose-content'); - if (editor && !editor.isHidden()) { - return editor.getContent(); - } - } - var textarea = document.querySelector('#compose-content'); - return textarea ? textarea.value : ''; - } - - function bindComposeDraft(form) { - var draftButton = form.querySelector('#compose-draft'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - - if (!draftButton || !ajaxUrl || !nonce) { - return; - } - - draftButton.addEventListener('click', function () { - draftButton.disabled = true; - setFeedback(feedback, '正在保存草稿…', 'is-loading'); - - var data = new FormData(form); - data.set('content', getEditorContent()); - data.append('action', 'aksrt_forum_save_draft'); - data.append('_nonce', nonce); - - var draftId = form.getAttribute('data-draft-id') || '0'; - if (draftId && draftId !== '0') { - data.append('draft_id', draftId); - } - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '保存失败'); - } - if (payload.data && payload.data.post_id) { - form.setAttribute('data-draft-id', String(payload.data.post_id)); - } - setFeedback(feedback, payload.data.message || '草稿已保存', 'is-success'); - draftButton.disabled = false; - }).catch(function (error) { - setFeedback(feedback, error.message || '保存失败', 'is-error'); - draftButton.disabled = false; - }); - }); - } - - function bindComposeSubmit(form) { - var submitButton = form.querySelector('#compose-submit'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - - if (!submitButton || !ajaxUrl || !nonce) { - return; - } - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var board = form.querySelector('#compose-board') || form.querySelector('[name="board_id"]'); - var title = form.querySelector('#compose-title'); - var content = getEditorContent(); - - if (!board || !board.value || !title || !title.value.trim() || !content.trim()) { - setFeedback(feedback, '请填写版块、标题和主题内容。', 'is-error'); - return; - } - - submitButton.disabled = true; - setFeedback(feedback, '正在提交主题…', 'is-loading'); - - var data = new FormData(form); - data.set('content', content); - data.append('action', 'aksrt_forum_create_topic'); - data.append('_nonce', nonce); - - var draftId = form.getAttribute('data-draft-id') || '0'; - if (draftId && draftId !== '0') { - data.append('draft_id', draftId); - } - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (response) { - return response.json(); - }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '提交失败'); - } - - if (payload.data && payload.data.redirect) { - window.location.href = payload.data.redirect; - return; - } - - setFeedback(feedback, (payload.data && payload.data.message) || '主题已提交。', 'is-success'); - }).catch(function (error) { - setFeedback(feedback, error.message || '提交失败,请稍后重试。', 'is-error'); - submitButton.disabled = false; - }); - }); - } - - function bindModeratorApplyForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var boardId = parseInt(form.getAttribute('data-board-id') || '0', 10); - var reasonField = form.querySelector('[name="reason"]'); - var reason = reasonField ? reasonField.value.trim() : ''; - - if (!boardId || !reason) { - setFeedback(feedback, '请填写完整的申请理由。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - setFeedback(feedback, '正在提交申请…', 'is-loading'); - - request('/forum/moderator/apply', { - method: 'POST', - body: JSON.stringify({ - board_id: boardId, - reason: reason, - }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '申请已提交', 'is-success'); - if (submitButton) submitButton.disabled = true; - // Disable the form to prevent re-submission - var reasonField = form.querySelector('[name="reason"]'); - if (reasonField) reasonField.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '申请提交失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function bindVoteForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var topicId = parseInt(form.getAttribute('data-topic-id') || '0', 10); - var checked = Array.prototype.slice.call(form.querySelectorAll('input[name="option_ids[]"]:checked')).map(function (input) { - return input.value; - }); - - if (!topicId || checked.length === 0) { - setFeedback(feedback, '请选择至少一个投票选项。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - setFeedback(feedback, '正在提交投票…', 'is-loading'); - - request('/forum/topics/' + topicId + '/vote', { - method: 'POST', - body: JSON.stringify({ option_ids: checked }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '投票成功', 'is-success'); - // Disable all checkboxes and submit after vote - var checkboxes = form.querySelectorAll('input[type="checkbox"]'); - for (var i = 0; i < checkboxes.length; i++) checkboxes[i].disabled = true; - if (submitButton) submitButton.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '投票失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function buildModerationPayload(form, action) { - var noteField = form.querySelector('[name="note"]'); - var payload = { - action: action, - note: noteField ? noteField.value.trim() : '', - }; - - if (action === 'review') { - payload.review_status = (form.querySelector('[name="review_status"]') || {}).value || ''; - } else if (action === 'move_board') { - payload.target_board_id = parseInt((form.querySelector('[name="target_board_id"]') || {}).value || '0', 10); - } else if (action === 'visibility') { - payload.visibility = (form.querySelector('[name="visibility"]') || {}).value || ''; - if (payload.visibility === 'credits') { - payload.credits_cost = parseInt((form.querySelector('[name="credits_cost"]') || {}).value || '0', 10); - } else if (payload.visibility === 'pay') { - payload.pay_price = (form.querySelector('[name="pay_price"]') || {}).value || ''; - } else if (payload.visibility === 'role') { - payload.visibility_roles = Array.prototype.slice.call(form.querySelectorAll('[name="visibility_roles[]"] option:checked')).map(function (option) { - return option.value; - }); - } - } else if (action === 'sticky') { - // 置顶分级(§7.3):发送 0~3 整数,而非布尔值。 - payload.value = parseInt((form.querySelector('[name="sticky_value"]') || {}).value || '0', 10); - } else if (action === 'question_status') { - payload.value = parseInt((form.querySelector('[name="question_status_value"]') || {}).value || '0', 10); - } else if (action === 'delete' || action === 'undelete') { - payload.value = true; - } else { - var valueFieldName = action + '_value'; - payload.value = ((form.querySelector('[name="' + valueFieldName + '"]') || {}).value || '0') === '1'; - } - - return payload; - } - - function bindModerationFieldToggle(form) { - var actionField = form.querySelector('.js-aksrt-moderation-action'); - var groups = form.querySelectorAll('.js-aksrt-moderation-field'); - var visibilitySelect = form.querySelector('[name="visibility"]'); - var visibilityExtras = form.querySelectorAll('.js-aksrt-visibility-extra'); - if (!actionField || !groups.length) { - return; - } - - function syncVisibilityExtras() { - if (!visibilitySelect || !visibilityExtras.length) { - return; - } - var visibility = visibilitySelect.value; - visibilityExtras.forEach(function (group) { - group.hidden = actionField.value !== 'visibility' || group.getAttribute('data-visibility-field') !== visibility; - }); - } - - function sync() { - var action = actionField.value; - groups.forEach(function (group) { - var fieldName = group.getAttribute('data-field'); - if (group.classList.contains('js-aksrt-visibility-extra')) { - return; - } - group.hidden = fieldName !== action; - }); - syncVisibilityExtras(); - } - - actionField.addEventListener('change', sync); - if (visibilitySelect) { - visibilitySelect.addEventListener('change', syncVisibilityExtras); - } - sync(); - } - - function bindModerationForm(form) { - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - bindModerationFieldToggle(form); - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var topicId = parseInt(form.getAttribute('data-topic-id') || '0', 10); - var actionField = form.querySelector('.js-aksrt-moderation-action'); - var action = actionField ? actionField.value : ''; - if (!topicId || !action) { - setFeedback(feedback, '请选择治理动作。', 'is-error'); - return; - } - - if (submitButton) { - submitButton.disabled = true; - } - if (action === 'delete' && !window.confirm('确认将该主题移入回收站吗?')) { - if (submitButton) submitButton.disabled = false; - return; - } - if (action === 'undelete' && !window.confirm('确认恢复该主题吗?')) { - if (submitButton) submitButton.disabled = false; - return; - } - setFeedback(feedback, '正在执行治理动作…', 'is-loading'); - - request('/forum/topics/' + topicId + '/moderate', { - method: 'POST', - body: JSON.stringify(buildModerationPayload(form, action)), - }).then(function (payload) { - setFeedback(feedback, payload.message || '治理动作已执行', 'is-success'); - if (submitButton) submitButton.disabled = true; - }).catch(function (error) { - setFeedback(feedback, error.message || '治理动作执行失败。', 'is-error'); - if (submitButton) { - submitButton.disabled = false; - } - }); - }); - } - - function bindQuickTopicAction(button) { - button.addEventListener('click', function () { - var topicId = parseInt(button.getAttribute('data-topic-id') || '0', 10); - var action = button.getAttribute('data-action') || ''; - var rawValue = button.getAttribute('data-value') || '0'; - var valueType = button.getAttribute('data-value-type') || 'bool'; - // 置顶分级(§7.3):data-value-type="int" 时发送整数级别,否则发送布尔值。 - var value = valueType === 'int' ? parseInt(rawValue, 10) : rawValue === '1'; - - if (!topicId || !action) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/moderate', { - method: 'POST', - body: JSON.stringify({ - action: action, - value: value, - }), - }).then(function () { - // Toggle button visual state instead of reloading - var icon = button.querySelector('i'); - if (valueType === 'int' ? value > 0 : value) { - button.classList.add('is-active'); - if (icon) icon.className = icon.className.replace('far', 'fas'); - } else { - button.classList.remove('is-active'); - if (icon) icon.className = icon.className.replace('fas', 'far'); - } - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '治理动作执行失败。'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindFavoriteButton(button) { - button.addEventListener('click', function () { - var topicId = button.getAttribute('data-topic-id'); - if (!topicId || button.disabled) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/favorite', { - method: 'POST', - body: JSON.stringify({}), - }).then(function (payload) { - var isFav = !!(payload.favorited); - var icon = button.querySelector('i'); - var label = button.querySelector('.js-aksrt-favorite-label'); - button.setAttribute('data-favorited', isFav ? '1' : '0'); - if (icon) { - icon.className = isFav ? 'fas fa-bookmark' : 'far fa-bookmark'; - } - if (label) { - label.textContent = isFav ? '已收藏' : '收藏'; - } - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '操作失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindScoreButton(button) { - button.addEventListener('click', function () { - var container = button.closest('.aksrt-forum-topic__score-actions'); - if (!container || button.disabled) { - return; - } - - var topicId = container.getAttribute('data-topic-id'); - var amount = parseInt(button.getAttribute('data-amount') || '0', 10); - var feedback = container.querySelector('.js-aksrt-action-feedback'); - - if (!topicId || amount === 0) { - return; - } - - button.disabled = true; - setFeedback(feedback, '提交中…', 'is-loading'); - - request('/forum/topics/' + topicId + '/score', { - method: 'POST', - body: JSON.stringify({ amount: amount }), - }).then(function (payload) { - var scoreEl = container.querySelector('.js-aksrt-score-value'); - if (scoreEl && payload.score !== undefined) { - scoreEl.textContent = String(payload.score); - } - setFeedback(feedback, payload.message || '评分成功', 'is-success'); - // Disable both score buttons after success (one-time per user) - container.querySelectorAll('.js-aksrt-topic-score').forEach(function (btn) { - btn.disabled = true; - }); - }).catch(function (error) { - setFeedback(feedback, error.message || '评分失败', 'is-error'); - button.disabled = false; - }); - }); - } - - function bindAdoptButton(button) { - button.addEventListener('click', function () { - var topicId = button.getAttribute('data-topic-id'); - var commentId = button.getAttribute('data-comment-id'); - if (!topicId || !commentId || button.disabled) { - return; - } - - if (!window.confirm('确认采纳该回复为最佳答案?')) { - return; - } - - button.disabled = true; - request('/forum/topics/' + topicId + '/adopt', { - method: 'POST', - body: JSON.stringify({ comment_id: parseInt(commentId, 10) }), - }).then(function (payload) { - button.textContent = '✓ 已采纳'; - button.classList.add('is-adopted'); - // Disable all other adopt buttons - document.querySelectorAll('.js-aksrt-adopt-answer').forEach(function (btn) { - btn.disabled = true; - }); - }).catch(function (error) { - var msg = error.message || '采纳失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindReportModal(modal) { - var form = modal.querySelector('.js-aksrt-report-form'); - var feedback = modal.querySelector('.js-aksrt-action-feedback'); - var targetLabel = modal.querySelector('.js-aksrt-report-target'); - var targetTypeField = form ? form.querySelector('[name="target_type"]') : null; - var targetIdField = form ? form.querySelector('[name="target_id"]') : null; - var closeButtons = modal.querySelectorAll('.js-aksrt-report-close'); - - function openModal(trigger) { - if (!form) return; - var targetType = trigger.getAttribute('data-target-type') || 'post'; - var targetId = trigger.getAttribute('data-target-id') || ''; - var targetTitle = trigger.getAttribute('data-target-title') || ''; - if (targetTypeField) targetTypeField.value = targetType; - if (targetIdField) targetIdField.value = targetId; - if (targetLabel) { - targetLabel.textContent = '举报对象:' + (targetTitle || ('#' + targetId)); - } - modal.hidden = false; - document.body.classList.add('is-report-modal-open'); - } - - function closeModal() { - modal.hidden = true; - document.body.classList.remove('is-report-modal-open'); - if (form) form.reset(); - setFeedback(feedback, '', ''); - } - - document.querySelectorAll('.js-aksrt-report-trigger').forEach(function (trigger) { - trigger.addEventListener('click', function () { - openModal(trigger); - }); - }); - - closeButtons.forEach(function (btn) { - btn.addEventListener('click', closeModal); - }); - - if (!form) return; - - form.addEventListener('submit', function (event) { - event.preventDefault(); - var reason = (form.querySelector('[name="reason"]') || {}).value || ''; - if (!reason.trim()) { - setFeedback(feedback, '请填写举报说明。', 'is-error'); - return; - } - setFeedback(feedback, '正在提交举报…', 'is-loading'); - request('/report/submit', { - method: 'POST', - body: JSON.stringify({ - target_type: targetTypeField ? targetTypeField.value : 'post', - target_id: parseInt(targetIdField ? targetIdField.value : '0', 10), - category: (form.querySelector('[name="category"]') || {}).value || 'other', - reason: reason, - }), - }).then(function (payload) { - setFeedback(feedback, payload.message || '举报已提交', 'is-success'); - setTimeout(closeModal, 900); - }).catch(function (error) { - setFeedback(feedback, error.message || '举报提交失败。', 'is-error'); - }); - }); - } - - - function insertForumEditorContent(html) { - if (window.tinymce) { - var ed = window.tinymce.get('compose-content') || window.tinymce.get('post_content'); - if (ed && !ed.isHidden()) { - ed.focus(); - ed.execCommand('mceInsertContent', false, html); - return; - } - } - var ta = document.getElementById('compose-content') || document.getElementById('post_content'); - if (!ta) return; - var start = ta.selectionStart || 0; - var end = ta.selectionEnd || 0; - var val = ta.value || ''; - ta.value = val.slice(0, start) + html + val.slice(end); - ta.focus(); - } - - function bindComposeShortcodeTools(root) { - root = root || document; - root.addEventListener('click', function (e) { - var btn = e.target.closest('[data-insert-shortcode]'); - if (!btn) return; - e.preventDefault(); - var type = btn.getAttribute('data-insert-shortcode'); - if (type === 'hide') { - insertForumEditorContent('[hidecontent type="login"]隐藏内容[/hidecontent]'); - } else if (type === 'reply') { - insertForumEditorContent('[hidecontent type="reply"]回复后可见[/hidecontent]'); - } else if (type === 'payshow') { - insertForumEditorContent('[hidecontent type="payshow"]付费可见内容[/hidecontent]'); - } else if (type === 'vip') { - insertForumEditorContent('[vip]VIP 可见内容[/vip]'); - } - }); - } - - function bindComposeVisibilityToggle(form) { - var visSelect = form.querySelector('#compose-visibility'); - var creditsField = form.querySelector('#compose-credits-cost-field'); - var payField = form.querySelector('#compose-pay-price-field'); - var roleField = form.querySelector('#compose-role-field'); - - if (!visSelect) return; - - function syncVisFields() { - var val = visSelect.value; - if (creditsField) creditsField.style.display = val === 'credits' ? '' : 'none'; - if (payField) payField.style.display = val === 'pay' ? '' : 'none'; - if (roleField) roleField.style.display = val === 'role' ? '' : 'none'; - } - - visSelect.addEventListener('change', syncVisFields); - syncVisFields(); - } - - function bindUnlockButton(button) { - button.addEventListener('click', function () { - var topicId = parseInt(button.getAttribute('data-topic-id') || '0', 10); - var cost = button.getAttribute('data-cost') || '0'; - var feedback = button.parentElement ? button.parentElement.querySelector('.js-aksrt-action-feedback') : null; - - if (!topicId || !window.confirm('确认使用 ' + cost + ' 积分解锁此主题?')) { - return; - } - - button.disabled = true; - setFeedback(feedback, '正在解锁…', 'is-loading'); - - var data = new FormData(); - data.append('action', 'aksrt_forum_unlock_topic'); - data.append('topic_id', topicId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '解锁失败'); - } - setFeedback(feedback, '解锁成功,正在刷新…', 'is-success'); - setTimeout(function () { window.location.reload(); }, 800); - }).catch(function (error) { - setFeedback(feedback, error.message || '解锁失败', 'is-error'); - button.disabled = false; - }); - }); - } - - function formatFollowPayLabel(pay) { - pay = pay || {}; - if (pay.is_free) { - return 'VIP 免费关注'; - } - if (pay.label) { - return String(pay.label); - } - if (pay.modo === 'points') { - return String(pay.amount || 0) + ' 积分'; - } - return '¥' + String(pay.amount || '0'); - } - - function purchasePlateFollow(plateId, button, payHint) { - payHint = payHint || {}; - var modo = String(payHint.modo || 'money'); - - // Cash (paid or VIP free): multi-gateway cashier — free VIP still creates 0-order for ledger. - // No Credits×100 bypass. - if (modo === 'money') { - button.disabled = false; - if (typeof window.aksrtOpenCashier === 'function') { - window.aksrtOpenCashier(plateId, button, '', '', 'plate_follow'); - return; - } - var msgCashier = '收银台未加载,请刷新页面后重试'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msgCashier, 'error'); } - else { window.alert(msgCashier); } - return; - } - - // Points / points VIP free: keep AJAX purchase path. - var confirmMsg = '确认付费关注该版块?'; - if (payHint.is_free) { - confirmMsg = '确认使用 VIP 免费关注该版块?'; - } else { - confirmMsg = '确认支付 ' + formatFollowPayLabel(payHint) + ' 关注该版块?'; - } - if (!window.confirm(confirmMsg)) { - button.disabled = false; - return; - } - - var data = new FormData(); - data.append('action', 'aksrt_forum_purchase_plate_follow'); - data.append('plate_id', plateId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - var errData = payload.data || {}; - // Server may redirect money path to cashier. - if (errData.require_cashier && typeof window.aksrtOpenCashier === 'function') { - button.disabled = false; - window.aksrtOpenCashier(plateId, button, '', '', 'plate_follow'); - return; - } - throw new Error(errData.message || '支付失败'); - } - var res = payload.data || {}; - button.setAttribute('data-followed', '1'); - button.classList.add('is-followed'); - if (button.querySelector('i') && button.childNodes.length) { - button.innerHTML = ' 已关注'; - } else { - button.textContent = '已关注'; - } - if (typeof window.aksrtToast === 'function') { - window.aksrtToast(res.message || '关注成功', 'success'); - } - setTimeout(function () { window.location.reload(); }, 600); - }).catch(function (error) { - var msg = error.message || '支付失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - } - - function bindFollowPlateButton(button) { - button.addEventListener('click', function () { - var plateId = parseInt(button.getAttribute('data-plate-id') || '0', 10); - if (!plateId || button.disabled) { - return; - } - button.disabled = true; - - var data = new FormData(); - data.append('action', 'aksrt_forum_follow_plate'); - data.append('plate_id', plateId); - data.append('_nonce', rootData.forumNonce || ''); - - fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - // Paid follow required - if (!payload.success && payload.data && payload.data.require_pay) { - purchasePlateFollow(plateId, button, payload.data.pay || {}); - return; - } - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '操作失败'); - } - var res = payload.data || {}; - var followed = !!res.followed; - button.setAttribute('data-followed', followed ? '1' : '0'); - button.classList.toggle('is-followed', followed); - button.innerHTML = followed - ? ' 已关注' - : ' 关注'; - button.disabled = false; - }).catch(function (error) { - var msg = error.message || '操作失败'; - if (typeof window.aksrtToast === 'function') { window.aksrtToast(msg, 'error'); } - else { window.alert(msg); } - button.disabled = false; - }); - }); - } - - function bindComposeCoverToggle(form) { - var coverTypeSelect = form.querySelector('#compose-cover-type'); - var imageField = form.querySelector('#compose-cover-image-field'); - var slideField = form.querySelector('#compose-cover-slide-field'); - var videoField = form.querySelector('#compose-cover-video-field'); - - if (!coverTypeSelect) return; - - function syncCoverFields() { - var val = coverTypeSelect.value; - if (imageField) imageField.style.display = val === 'image' ? '' : 'none'; - if (slideField) slideField.style.display = val === 'slide' ? '' : 'none'; - if (videoField) videoField.style.display = val === 'video' ? '' : 'none'; - } - - coverTypeSelect.addEventListener('change', syncCoverFields); - syncCoverFields(); - - var imageInput = form.querySelector('#compose-cover-image'); - var imagePreview = form.querySelector('#compose-cover-preview'); - var imageBtn = form.querySelector('#compose-cover-upload-btn'); - - if (imageBtn && typeof wp !== 'undefined' && wp.media) { - imageBtn.addEventListener('click', function () { - var frame = wp.media({ title: '选择封面图片', multiple: false, library: { type: 'image' } }); - frame.on('select', function () { - var attachment = frame.state().get('selection').first().toJSON(); - if (imageInput) imageInput.value = attachment.url; - if (imagePreview) imagePreview.innerHTML = ''; - }); - frame.open(); - }); - } - - var slideInput = form.querySelector('#compose-featured-slide'); - var slidePreview = form.querySelector('#compose-slide-preview'); - var slideBtn = form.querySelector('#compose-slide-upload-btn'); - - if (slideBtn && typeof wp !== 'undefined' && wp.media) { - slideBtn.addEventListener('click', function () { - var frame = wp.media({ title: '选择幻灯片图片', multiple: true, library: { type: 'image' } }); - frame.on('select', function () { - var urls = frame.state().get('selection').map(function (a) { return a.toJSON().url; }); - urls = urls.slice(0, 9); - if (slideInput) slideInput.value = JSON.stringify(urls); - if (slidePreview) { - slidePreview.innerHTML = urls.map(function (u) { return ''; }).join(''); - } - }); - frame.open(); - }); - } - } - - function bindEditPlateButton(button) { - button.addEventListener('click', function () { - var plateId = button.getAttribute('data-plate-id'); - var name = window.prompt('版块名称:', button.getAttribute('data-name') || ''); - if (name === null) return; - var description = window.prompt('版块描述:', button.getAttribute('data-description') || ''); - if (description === null) description = ''; - - request('/forum/front/plates/' + plateId, { - method: 'POST', - body: JSON.stringify({ name: name, description: description }), - }).then(function () { - window.location.reload(); - }).catch(function (error) { - window.alert(error.message || '编辑失败'); - }); - }); - } - - function bindDeletePlateButton(button) { - button.addEventListener('click', function () { - var plateId = button.getAttribute('data-plate-id'); - var topicCount = parseInt(button.getAttribute('data-topic-count') || '0', 10); - var msg = '确认删除此版块?'; - if (topicCount > 0) { - msg += '\n该版块下有 ' + topicCount + ' 个主题,删除后将移入回收站。'; - } - if (!window.confirm(msg)) return; - - request('/forum/front/plates/' + plateId, { - method: 'DELETE', - body: JSON.stringify({ cascade: 'trash' }), - }).then(function () { - window.location.href = (rootData.siteUrl || '') + '/forums/'; - }).catch(function (error) { - window.alert(error.message || '删除失败'); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Remote search selector - // ────────────────────────────────────────────────────────── - - function bindRemoteSelects(container) { - var selects = container.querySelectorAll('.aksrt-remote-select'); - selects.forEach(function (wrapper) { - var searchAction = wrapper.getAttribute('data-search-action'); - var hiddenInput = wrapper.querySelector('input[type="hidden"]'); - var textInput = wrapper.querySelector('.js-aksrt-remote-input'); - var dropdown = wrapper.querySelector('.aksrt-remote-select__dropdown'); - if (!searchAction || !hiddenInput || !textInput || !dropdown) return; - - var debounceTimer = null; - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - - textInput.addEventListener('input', function () { - clearTimeout(debounceTimer); - var q = textInput.value.trim(); - debounceTimer = setTimeout(function () { - fetch(ajaxUrl + '?action=' + encodeURIComponent(searchAction) + '&q=' + encodeURIComponent(q), { - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success || !payload.data) return; - dropdown.innerHTML = ''; - payload.data.forEach(function (item) { - var div = document.createElement('div'); - div.className = 'aksrt-remote-select__option'; - div.textContent = item.name; - div.setAttribute('data-id', String(item.id)); - div.addEventListener('mousedown', function (e) { - e.preventDefault(); - hiddenInput.value = item.id; - textInput.value = item.name; - dropdown.hidden = true; - }); - dropdown.appendChild(div); - }); - dropdown.hidden = payload.data.length === 0; - }); - }, 250); - }); - - textInput.addEventListener('focus', function () { - if (dropdown.children.length > 0) dropdown.hidden = false; - textInput.dispatchEvent(new Event('input')); - }); - textInput.addEventListener('blur', function () { - setTimeout(function () { dropdown.hidden = true; }, 200); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Tag tokenizer - // ────────────────────────────────────────────────────────── - - function bindTagInputs(container) { - var wrappers = container.querySelectorAll('.aksrt-tag-input'); - wrappers.forEach(function (wrapper) { - var searchAction = wrapper.getAttribute('data-search-action'); - var maxTags = parseInt(wrapper.getAttribute('data-max') || '5', 10); - var hiddenInput = wrapper.querySelector('input[type="hidden"]'); - var tokenContainer = wrapper.querySelector('.aksrt-tag-input__tokens'); - var textInput = wrapper.querySelector('.js-aksrt-tag-search'); - var dropdown = wrapper.querySelector('.aksrt-tag-input__dropdown'); - if (!hiddenInput || !tokenContainer || !textInput) return; - - var tags = []; - var initialVal = (hiddenInput.value || '').trim(); - if (initialVal) { - initialVal.split(/[,,]/).forEach(function (t) { - t = t.trim(); - if (t && tags.indexOf(t) === -1) tags.push(t); - }); - } - - function syncHidden() { - hiddenInput.value = tags.join(', '); - } - - function renderTokens() { - tokenContainer.innerHTML = ''; - tags.forEach(function (tag, idx) { - var span = document.createElement('span'); - span.className = 'aksrt-tag-input__token'; - span.textContent = tag; - var close = document.createElement('button'); - close.type = 'button'; - close.className = 'aksrt-tag-input__token-remove'; - close.innerHTML = '×'; - close.addEventListener('click', function () { - tags.splice(idx, 1); - syncHidden(); - renderTokens(); - }); - span.appendChild(close); - tokenContainer.appendChild(span); - }); - } - - function addTag(name) { - name = name.trim(); - if (!name || tags.indexOf(name) !== -1 || tags.length >= maxTags) return; - tags.push(name); - syncHidden(); - renderTokens(); - textInput.value = ''; - } - - renderTokens(); - - textInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - addTag(textInput.value); - if (dropdown) dropdown.hidden = true; - } - if (e.key === 'Backspace' && textInput.value === '' && tags.length > 0) { - tags.pop(); - syncHidden(); - renderTokens(); - } - }); - - if (searchAction && dropdown) { - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - var debounce = null; - - textInput.addEventListener('input', function () { - clearTimeout(debounce); - var q = textInput.value.trim(); - if (!q) { dropdown.hidden = true; return; } - debounce = setTimeout(function () { - fetch(ajaxUrl + '?action=' + encodeURIComponent(searchAction) + '&q=' + encodeURIComponent(q), { - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success || !payload.data) return; - dropdown.innerHTML = ''; - payload.data.forEach(function (item) { - var div = document.createElement('div'); - div.className = 'aksrt-tag-input__suggestion'; - div.textContent = item.name + (item.count !== undefined ? ' (' + item.count + ')' : ''); - div.addEventListener('mousedown', function (e) { - e.preventDefault(); - addTag(item.name); - dropdown.hidden = true; - }); - dropdown.appendChild(div); - }); - dropdown.hidden = payload.data.length === 0; - }); - }, 250); - }); - - textInput.addEventListener('blur', function () { - setTimeout(function () { dropdown.hidden = true; }, 200); - }); - } - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Title character counter - // ────────────────────────────────────────────────────────── - - function bindTitleCounter(form) { - var titleInput = form.querySelector('#compose-title'); - var counterCurrent = form.querySelector('.js-aksrt-counter-current'); - if (!titleInput || !counterCurrent) return; - - function update() { - counterCurrent.textContent = String(titleInput.value.length); - var max = parseInt(titleInput.getAttribute('data-aksrt-maxlength') || '30', 10); - var parent = counterCurrent.closest('.aksrt-forum-compose__char-counter'); - if (parent) { - parent.classList.toggle('is-over', titleInput.value.length > max); - parent.classList.toggle('is-near', titleInput.value.length > max * 0.8 && titleInput.value.length <= max); - } - } - - titleInput.addEventListener('input', update); - update(); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Draft list toggle & delete - // ────────────────────────────────────────────────────────── - - function bindDraftUI() { - var toggle = document.querySelector('.js-aksrt-draft-toggle'); - var list = document.querySelector('#compose-draft-list'); - if (toggle && list) { - toggle.addEventListener('click', function () { - list.hidden = !list.hidden; - var icon = toggle.querySelector('i.fa-chevron-down, i.fa-chevron-up'); - if (icon) { - icon.classList.toggle('fa-chevron-down'); - icon.classList.toggle('fa-chevron-up'); - } - }); - } - - document.querySelectorAll('.js-aksrt-delete-draft').forEach(function (btn) { - btn.addEventListener('click', function () { - var draftId = btn.getAttribute('data-draft-id'); - if (!draftId || !window.confirm('确认删除此草稿?')) return; - - var ajaxUrl = (rootData.ajaxUrl || '/wp-admin/admin-ajax.php'); - var nonce = ''; - var form = document.querySelector('.js-aksrt-forum-compose-form'); - if (form) nonce = form.getAttribute('data-nonce') || ''; - - var data = new FormData(); - data.append('action', 'aksrt_forum_delete_draft'); - data.append('_nonce', nonce); - data.append('draft_id', draftId); - - fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }) - .then(function (r) { - return r.text().then(function (text) { - var payload = null; - try { - payload = text ? JSON.parse(text) : null; - } catch (e) { - payload = null; - } - return { ok: r.ok, status: r.status, payload: payload, raw: text }; - }); - }) - .then(function (res) { - if (res.payload && res.payload.success) { - var item = btn.closest('.aksrt-forum-compose__draft-item'); - if (item) item.remove(); - return; - } - var msg = (res.payload && res.payload.data && res.payload.data.message) - || (res.payload && res.payload.message) - || (res.status === 500 - ? '服务器错误:请确认主题已更新到最新 main(Forum.php 草稿 autoload 修复)' - : '删除失败'); - if (window.console && console.warn) { - console.warn('[aksrt] delete draft failed', res.status, msg); - } - window.alert(msg); - }) - .catch(function (err) { - if (window.console && console.warn) { - console.warn('[aksrt] delete draft network error', err); - } - window.alert('删除失败,请稍后重试'); - }); - }); - }); - } - - // ────────────────────────────────────────────────────────── - // §7.4 — Edit form AJAX submit - // ────────────────────────────────────────────────────────── - - function bindEditForm(form) { - var submitButton = form.querySelector('#compose-submit'); - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var ajaxUrl = form.getAttribute('data-ajax'); - var nonce = form.getAttribute('data-nonce'); - var topicId = form.getAttribute('data-topic-id'); - - if (!submitButton || !ajaxUrl || !nonce || !topicId) return; - - form.addEventListener('submit', function (event) { - event.preventDefault(); - - var title = form.querySelector('#compose-title'); - var content = getEditorContent(); - - if (!title || !title.value.trim() || !content.trim()) { - setFeedback(feedback, '标题和内容不能为空。', 'is-error'); - return; - } - - submitButton.disabled = true; - setFeedback(feedback, '正在保存…', 'is-loading'); - - var data = new FormData(form); - data.set('content', content); - data.append('action', 'aksrt_forum_edit_topic'); - data.append('_nonce', nonce); - data.append('topic_id', topicId); - - fetch(ajaxUrl, { - method: 'POST', - body: data, - credentials: 'same-origin', - }).then(function (r) { return r.json(); }).then(function (payload) { - if (!payload.success) { - throw new Error((payload.data && payload.data.message) || '保存失败'); - } - if (payload.data && payload.data.redirect) { - window.location.href = payload.data.redirect; - return; - } - setFeedback(feedback, payload.data.message || '已保存', 'is-success'); - submitButton.disabled = false; - }).catch(function (error) { - setFeedback(feedback, error.message || '保存失败', 'is-error'); - submitButton.disabled = false; - }); - }); - } - - // ── §7.6 Forum comment engine ───────────────────────────────────────── - - function bindForumCommentEngine() { - var section = document.getElementById('aksrt-forum-replies'); - if (!section) return; - - var topicId = section.dataset.topicId; - var nonce = section.dataset.nonce; - var list = section.querySelector('.js-aksrt-forum-replies-list'); - var loadMore = section.querySelector('.js-aksrt-forum-load-more'); - var currentSort = 'newest'; - var currentAuthorOnly = false; - - // Sort tabs - section.querySelectorAll('.aksrt-forum-replies__tab').forEach(function (tab) { - tab.addEventListener('click', function () { - section.querySelectorAll('.aksrt-forum-replies__tab').forEach(function (t) { t.classList.remove('is-active'); }); - tab.classList.add('is-active'); - currentSort = tab.dataset.sort || 'newest'; - loadComments(1); - }); - }); - - // Author-only toggle - var authorToggle = section.querySelector('.aksrt-forum-replies__author-toggle'); - if (authorToggle) { - authorToggle.addEventListener('change', function () { - currentAuthorOnly = authorToggle.checked; - loadComments(1); - }); - } - - // Load more button - if (loadMore) { - loadMore.addEventListener('click', function () { - var page = parseInt(loadMore.dataset.page, 10) || 1; - loadComments(page + 1, true); - }); - } - - function loadComments(page, append) { - var fd = new FormData(); - fd.append('action', 'aksrt_forum_comments'); - fd.append('nonce', nonce); - fd.append('topic_id', topicId); - fd.append('sort', currentSort); - fd.append('author_only', currentAuthorOnly ? '1' : ''); - fd.append('page', page); - - list.style.opacity = append ? '1' : '0.5'; - - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : (window.ajaxurl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: fd, - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - list.style.opacity = '1'; - if (!res.success) return; - if (append) { - list.insertAdjacentHTML('beforeend', res.data.html); - } else { - list.innerHTML = res.data.html; - } - if (loadMore) { - loadMore.dataset.page = String(page); - loadMore.dataset.max = String(res.data.max_pages); - loadMore.closest('.aksrt-forum-replies__load-more').style.display = - res.data.has_more ? '' : 'none'; - } - bindDynamicCommentActions(list); - }).catch(function () { - list.style.opacity = '1'; - }); - } - - bindDynamicCommentActions(list); - } - - function bindDynamicCommentActions(container) { - if (!container) return; - container.querySelectorAll('.js-aksrt-comment-like').forEach(bindCommentLike); - container.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton); - container.querySelectorAll('.js-aksrt-comment-reply').forEach(bindCommentReply); - container.querySelectorAll('.js-aksrt-comment-top').forEach(bindCommentTop); - } - - function bindCommentLike(btn) { - if (btn._likebound) return; - btn._likebound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var active = btn.dataset.active === '1'; - var newActive = !active; - - btn.disabled = true; - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.restUrl + 'aksrt/v1/comment/like' : '/wp-json/aksrt/v1/comment/like', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-WP-Nonce': window.aksrtForumGovernance ? window.aksrtForumGovernance.restNonce : '' - }, - body: JSON.stringify({ comment_id: parseInt(commentId, 10), active: newActive }), - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - btn.disabled = false; - if (!res.success && !res.data) return; - var data = res.data || res; - btn.dataset.active = data.active ? '1' : '0'; - btn.classList.toggle('is-active', !!data.active); - var icon = btn.querySelector('i'); - if (icon) { - icon.className = data.active ? 'fas fa-thumbs-up' : 'far fa-thumbs-up'; - } - var count = btn.querySelector('.aksrt-forum-reply__like-count'); - if (count) { - count.textContent = data.count > 0 ? String(data.count) : ''; - } - }).catch(function () { btn.disabled = false; }); - }); - } - - function bindCommentReply(btn) { - if (btn._replybound) return; - btn._replybound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var author = btn.dataset.author || ''; - var form = document.querySelector('.aksrt-forum-topic__reply-form'); - if (!form) return; - var textarea = form.querySelector('textarea[name="comment"]'); - if (!textarea) return; - // Set reply parent - var parentInput = form.querySelector('input[name="comment_parent"]'); - if (!parentInput) { - parentInput = document.createElement('input'); - parentInput.type = 'hidden'; - parentInput.name = 'comment_parent'; - form.querySelector('form').appendChild(parentInput); - } - parentInput.value = commentId; - textarea.placeholder = '@' + author + ' '; - textarea.focus(); - form.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); - } - - function bindCommentTop(btn) { - if (btn._topbound) return; - btn._topbound = true; - btn.addEventListener('click', function () { - var commentId = btn.dataset.commentId; - var currentLevel = parseInt(btn.dataset.level, 10) || 0; - var newLevel = currentLevel > 0 ? 0 : 1; - - btn.disabled = true; - var section = document.getElementById('aksrt-forum-replies'); - var nonce = section ? section.dataset.nonce : ''; - - var fd = new FormData(); - fd.append('action', 'aksrt_forum_comment_top'); - fd.append('nonce', nonce); - fd.append('comment_id', commentId); - fd.append('level', newLevel); - - fetch(window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : (window.ajaxurl || '/wp-admin/admin-ajax.php'), { - method: 'POST', - body: fd, - credentials: 'same-origin' - }).then(function (r) { return r.json(); }) - .then(function (res) { - btn.disabled = false; - if (res.success) { - btn.dataset.level = String(newLevel); - var icon = btn.querySelector('i'); - btn.childNodes.forEach(function (n) { - if (n.nodeType === 3) n.textContent = newLevel > 0 ? ' 取消置顶' : ' 置顶'; - }); - // Reload comments to reflect pin ordering - var activeTab = section && section.querySelector('.aksrt-forum-replies__tab.is-active'); - if (activeTab) activeTab.click(); - } - }).catch(function () { btn.disabled = false; }); - }); - } - - // ── §7.10 Forum home tab engine ────────────────────────────────────── - - function bindForumHomeTabs() { - var nav = document.getElementById('aksrt-forum-home-tabs'); - if (!nav) return; - - var content = document.getElementById('aksrt-forum-home-content'); - if (!content) return; - - var nonce = nav.dataset.nonce || ''; - var ajaxUrl = nav.dataset.ajaxUrl || (window.aksrtForumGovernance ? window.aksrtForumGovernance.ajaxUrl : '/wp-admin/admin-ajax.php'); - - nav.querySelectorAll('.aksrt-forum-home-tabs__btn').forEach(function (btn) { - btn.addEventListener('click', function () { - var tabType = btn.dataset.tabType; - nav.querySelectorAll('.aksrt-forum-home-tabs__btn').forEach(function (b) { b.classList.remove('is-active'); }); - btn.classList.add('is-active'); - loadHomeTab(tabType, 1); - }); - }); - - // Load-more on home content - content.addEventListener('click', function (e) { - var moreBtn = e.target.closest('.js-aksrt-home-load-more'); - if (!moreBtn) return; - var page = parseInt(moreBtn.dataset.page, 10) || 1; - var tabType = moreBtn.dataset.tab || 'synthesis'; - loadHomeTab(tabType, page + 1, true, moreBtn); - }); - - function loadHomeTab(tabType, page, append, moreBtn) { - var fd = new FormData(); - fd.append('action', 'aksrt_forum_home_tab'); - fd.append('nonce', nonce); - fd.append('tab_type', tabType); - fd.append('page', page); - - if (!append) { - content.style.opacity = '0.5'; - } - if (moreBtn) moreBtn.disabled = true; - - fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - content.style.opacity = '1'; - if (!res.success) return; - - if (res.data.type === 'plate') { - // Plate tab — reload the whole page to get server-rendered board grid - window.location.href = window.location.pathname + '?forum_tab=plate'; - return; - } - - if (append) { - var list = content.querySelector('.js-aksrt-home-topic-list'); - if (list) { - list.insertAdjacentHTML('beforeend', res.data.html); - } - if (moreBtn) { - moreBtn.disabled = false; - moreBtn.dataset.page = String(page); - if (!res.data.has_more) { - moreBtn.closest('.aksrt-forum-home__load-more').style.display = 'none'; - } - } - } else { - var html = '
' + res.data.html + '
'; - if (res.data.has_more) { - html += '
' - + '
'; - } - content.innerHTML = html; - } - }).catch(function () { - content.style.opacity = '1'; - if (moreBtn) moreBtn.disabled = false; - }); - } - } - - - function bindBoardSortTabs() { - var root = document.querySelector('.aksrt-forum-board[data-plate-id], .aksrt-forum-board[data-board-id]'); - if (!root) return; - var list = root.querySelector('.js-aksrt-board-topic-list'); - var tabs = root.querySelectorAll('.js-aksrt-board-sort-tab'); - if (!list || !tabs.length) return; - - var plateId = root.getAttribute('data-plate-id') || ''; - var boardId = root.getAttribute('data-board-id') || ''; - var nonce = root.getAttribute('data-nonce') || (window.aksrtForumGovernance && aksrtForumGovernance.publicNonce) || (window.aksrtData && aksrtData.nonce) || ''; - var ajaxUrl = (window.aksrtForumGovernance && aksrtForumGovernance.ajaxUrl) || (window.aksrtData && aksrtData.ajaxUrl) || ''; - - function loadSort(sort, pushUrl) { - if (!ajaxUrl) return; - list.style.opacity = '0.55'; - var fd = new FormData(); - fd.append('action', 'aksrt_forum_board_topics'); - fd.append('nonce', nonce); - fd.append('page', '1'); - fd.append('orderby', sort); - if (plateId) fd.append('plate_id', plateId); - if (boardId) fd.append('board_id', boardId); - - fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - list.style.opacity = '1'; - if (!res || !res.success) return; - list.innerHTML = res.data.html || ''; - root.setAttribute('data-active-sort', sort); - tabs.forEach(function (t) { - t.classList.toggle('is-active', (t.getAttribute('data-sort') || '') === sort); - }); - if (pushUrl && window.history && history.replaceState) { - try { - var u = new URL(window.location.href); - u.searchParams.set('forum_sort', sort); - history.replaceState({}, '', u.toString()); - } catch (e) {} - } - }) - .catch(function () { list.style.opacity = '1'; }); - } - - tabs.forEach(function (tab) { - tab.addEventListener('click', function (e) { - e.preventDefault(); - var sort = tab.getAttribute('data-sort') || 'date'; - if (sort === (root.getAttribute('data-active-sort') || '')) return; - loadSort(sort, true); - }); - }); - } - - function boot() { - var composeForm = document.querySelector('.js-aksrt-forum-compose-form'); - if (composeForm) { - bindComposeToolbar(composeForm); - bindComposeVoteToggle(composeForm); - bindComposeCoverToggle(composeForm); - bindComposeVisibilityToggle(composeForm); - bindComposeDraft(composeForm); - bindComposeSubmit(composeForm); - bindRemoteSelects(composeForm); - bindTagInputs(composeForm); - bindTitleCounter(composeForm); - bindDraftUI(); - } - - var editForm = document.querySelector('.js-aksrt-forum-edit-form'); - if (editForm) { - bindComposeVoteToggle(editForm); - bindComposeCoverToggle(editForm); - bindComposeVisibilityToggle(editForm); - bindRemoteSelects(editForm); - bindTagInputs(editForm); - bindTitleCounter(editForm); - bindEditForm(editForm); - } - - document.querySelectorAll('.js-aksrt-moderator-apply-form').forEach(bindModeratorApplyForm); - document.querySelectorAll('.js-aksrt-topic-vote-form').forEach(bindVoteForm); - document.querySelectorAll('.js-aksrt-topic-moderation-form').forEach(bindModerationForm); - document.querySelectorAll('.js-aksrt-topic-action').forEach(bindQuickTopicAction); - document.querySelectorAll('.js-aksrt-topic-favorite').forEach(bindFavoriteButton); - document.querySelectorAll('.js-aksrt-topic-score').forEach(bindScoreButton); - document.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton); - document.querySelectorAll('.js-aksrt-unlock-topic').forEach(bindUnlockButton); - document.querySelectorAll('.js-aksrt-follow-plate').forEach(bindFollowPlateButton); - bindForumCommentEngine(); - bindForumHomeTabs(); - bindBoardSortTabs(); - document.querySelectorAll('.js-aksrt-edit-plate').forEach(bindEditPlateButton); - document.querySelectorAll('.js-aksrt-delete-plate').forEach(bindDeletePlateButton); - var reportModal = document.querySelector('#aksrt-report-modal'); - if (reportModal) { - bindReportModal(reportModal); - } - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', boot); - } else { - boot(); - } -})(); - diff --git a/assets/js/widget-set.js b/assets/js/widget-set.js index eab96e74..a3d68811 100644 --- a/assets/js/widget-set.js +++ b/assets/js/widget-set.js @@ -156,73 +156,6 @@ }); } - // Forum multi-tab topics. - function initForumTabTopics() { - document.querySelectorAll('.aksrt-ftab[data-aksrt-ftab]').forEach(function (root) { - if (root.dataset.aksrtFtabBound === '1') return; - root.dataset.aksrtFtabBound = '1'; - var rest = root.getAttribute('data-rest') || ''; - var count = root.getAttribute('data-count') || '6'; - var style = root.getAttribute('data-style') || 'mini'; - var plate = root.getAttribute('data-plate') || '0'; - var btns = [].slice.call(root.querySelectorAll('.aksrt-ftab__btn')); - var panels = [].slice.call(root.querySelectorAll('.aksrt-ftab__panel')); - - function load(panel, orderby) { - if (!panel || panel.getAttribute('data-loaded') === '1' || !rest) return; - var qs = new URLSearchParams({ - page: '1', - count: count, - orderby: orderby || 'date', - style: style, - plate: plate, - period: 'all', - paginate: 'none', - }); - var url = rest + (rest.indexOf('?') > -1 ? '&' : '?') + qs.toString(); - panel.classList.add('is-ajax-loading'); - fetch(url, { credentials: 'same-origin', headers: restHeaders() }) - .then(function (r) { return r.json(); }) - .then(function (j) { - var d = (j && j.data) || j || {}; - var html = d.html || ''; - panel.innerHTML = html || '
暂无内容
'; - panel.setAttribute('data-loaded', '1'); - panel.classList.remove('is-ajax-loading'); - }) - .catch(function () { - panel.classList.remove('is-ajax-loading'); - }); - } - - function go(i) { - btns.forEach(function (b, idx) { - var on = idx === i; - b.classList.toggle('is-active', on); - b.setAttribute('aria-selected', on ? 'true' : 'false'); - }); - panels.forEach(function (p, idx) { - var on = idx === i; - p.classList.toggle('is-active', on); - if (on) { - p.removeAttribute('hidden'); - } else { - p.setAttribute('hidden', ''); - } - }); - var btn = btns[i]; - var panel = panels[i]; - if (btn && panel) load(panel, btn.getAttribute('data-orderby') || 'date'); - } - - btns.forEach(function (b) { - b.addEventListener('click', function () { - go(parseInt(b.getAttribute('data-index') || '0', 10)); - }); - }); - }); - } - // Product multi-tab. function initProductTabs() { document.querySelectorAll('[data-aksrt-product-tab]').forEach(function (root) { @@ -383,86 +316,6 @@ } - function initModeratorApplyModal() { - document.addEventListener('click', function (e) { - var openBtn = e.target.closest('[data-aksrt-mod-apply-open]'); - if (openBtn) { - e.preventDefault(); - var id = openBtn.getAttribute('data-aksrt-mod-apply-open'); - var modal = id ? document.getElementById(id) : null; - if (modal) { - modal.hidden = false; - document.body.classList.add('aksrt-mod-apply-open'); - } - return; - } - var closeBtn = e.target.closest('[data-aksrt-mod-apply-close]'); - if (closeBtn) { - var box = closeBtn.closest('.aksrt-mod-apply-modal'); - if (box) { - box.hidden = true; - document.body.classList.remove('aksrt-mod-apply-open'); - } - } - }); - - document.querySelectorAll('.js-aksrt-moderator-apply-form').forEach(function (form) { - if (form.dataset.aksrtModApplyBound === '1') return; - form.dataset.aksrtModApplyBound = '1'; - form.addEventListener('submit', function (event) { - event.preventDefault(); - var boardId = parseInt(form.getAttribute('data-board-id') || '0', 10); - var reasonField = form.querySelector('[name="reason"]'); - var reason = reasonField ? reasonField.value.trim() : ''; - var feedback = form.querySelector('.js-aksrt-action-feedback'); - var submitButton = form.querySelector('button[type="submit"]'); - if (!boardId || !reason) { - if (feedback) { - feedback.textContent = '请填写完整的申请理由。'; - feedback.className = 'js-aksrt-action-feedback mb10 is-error'; - } - return; - } - if (submitButton) submitButton.disabled = true; - if (feedback) { - feedback.textContent = '正在提交申请…'; - feedback.className = 'js-aksrt-action-feedback mb10 is-loading'; - } - var siteUrl = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/+$/, '') : ''; - var url = siteUrl + '/wp-json/aksrt/v1/forum/moderator/apply'; - fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-WP-Nonce': (window.aksrtData && aksrtData.restNonce) || '', - }, - body: JSON.stringify({ board_id: boardId, reason: reason }), - }) - .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) - .then(function (res) { - if (!res.ok || (res.j && res.j.success === false)) { - throw new Error((res.j && res.j.message) || '申请提交失败'); - } - var msg = (res.j && (res.j.message || (res.j.data && res.j.data.message))) || '申请已提交'; - if (feedback) { - feedback.textContent = msg; - feedback.className = 'js-aksrt-action-feedback mb10 is-success'; - } - if (reasonField) reasonField.disabled = true; - }) - .catch(function (err) { - if (feedback) { - feedback.textContent = (err && err.message) || '申请提交失败。'; - feedback.className = 'js-aksrt-action-feedback mb10 is-error'; - } - if (submitButton) submitButton.disabled = false; - }); - }); - }); - } - function initFloatingQRCodes() { var wrap = document.querySelector('.float-right'); if (!wrap) return; @@ -508,11 +361,9 @@ function init() { initWidgetTabs(); initCategoryTabs(); - initForumTabTopics(); initProductTabs(); initMainPostsLazy(); initFeatureIntro(); - initModeratorApplyModal(); initFloatingQRCodes(); } diff --git a/footer.php b/footer.php index 8778740d..84ee0e82 100644 --- a/footer.php +++ b/footer.php @@ -396,12 +396,6 @@ if ($floating_enabled) : - 'fas fa-shopping-cart']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } ?> diff --git a/functions.php b/functions.php index c897bb0b..09f182ca 100644 --- a/functions.php +++ b/functions.php @@ -211,12 +211,6 @@ if (class_exists('\AKSRT\Content\Download')) { if (class_exists('\AKSRT\Content\Pay')) { \AKSRT\Content\Pay::get_instance()->init(); } -if (class_exists('\AKSRT\Forum\TopicPay')) { - \AKSRT\Forum\TopicPay::get_instance()->init(); -} -if (class_exists('\AKSRT\Forum\PlateFollowPay')) { - \AKSRT\Forum\PlateFollowPay::get_instance()->init(); -} if (class_exists('\AKSRT\Content\VipService')) { \AKSRT\Content\VipService::get_instance()->init(); } @@ -355,10 +349,7 @@ if (class_exists('\AKSRT\Payment\Gateway')) { do_action('aksrt_register_payment_gateways', $gw); } -if (class_exists('\AKSRT\Forum\Forum')) { - \AKSRT\Forum\Forum::get_instance()->init(); -} -if (is_admin() && class_exists('\AKSRT\Core\ResourceMetaBox')) { +if (class_exists('\AKSRT\Core\ResourceMetaBox')) { \AKSRT\Core\ResourceMetaBox::get_instance()->init(); } @@ -386,27 +377,6 @@ if (is_admin() && class_exists('\AKSRT\Shop\ProductVariantsMetaBox')) { \AKSRT\Shop\ProductVariantsMetaBox::get_instance()->init(); } -// 论坛版块版主 meta box -if (is_admin() && class_exists('\AKSRT\Forum\PlateMetaBox')) { - \AKSRT\Forum\PlateMetaBox::get_instance()->init(); -} -// 论坛帖子原生列表治理列/筛选/批量 -if (is_admin() && class_exists('\AKSRT\Forum\TopicAdminList')) { - \AKSRT\Forum\TopicAdminList::get_instance()->init(); -} -// 论坛帖子治理 meta box -if (is_admin() && class_exists('\AKSRT\Forum\TopicMetaBox')) { - \AKSRT\Forum\TopicMetaBox::get_instance()->init(); -} -// 论坛版块列表列 -if (is_admin() && class_exists('\AKSRT\Forum\PlateAdminList')) { - \AKSRT\Forum\PlateAdminList::get_instance()->init(); -} - - - - - // 商品评价/评分系统 if (class_exists('\AKSRT\Shop\ProductReview')) { \AKSRT\Shop\ProductReview::get_instance()->init(); diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 10e8ee03..eaac1682 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -38,7 +38,6 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-comments.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-forum.php'; -require_once AKSRT_DIR . '/inc/helpers/aksrt-forum-widgets.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-auth-ui.php'; diff --git a/inc/core-hooks/part-1.php b/inc/core-hooks/part-1.php index 98dfa457..2f821276 100644 --- a/inc/core-hooks/part-1.php +++ b/inc/core-hooks/part-1.php @@ -83,339 +83,4 @@ function aksrt_register_topics_taxonomy() { } add_action('init', 'aksrt_register_topics_taxonomy'); -// ============================================================ -// 注册自定义文章类型 - Forum Topic & 分类法 - Forum Board -// ============================================================ - -function aksrt_register_forum_cpt() { - // Respect bbs_s switch — skip CPT registration when forum is disabled. - // Admin context always registers so the backend settings page works. - if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - if (empty($forum['enabled'])) { - return; - } - } - - // Read custom slugs / names from settings when available - $plate_slug = 'board'; - $topic_slug = 'topic'; - $forum_enabled = true; - $forum_settings = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum_settings = \AKSRT\Settings\Settings::get_forum_settings(); - $plate_slug = !empty($forum_settings['other']['plate_slug']) ? $forum_settings['other']['plate_slug'] : 'board'; - $topic_slug = !empty($forum_settings['other']['posts_slug']) ? $forum_settings['other']['posts_slug'] : 'topic'; - $forum_enabled = !empty($forum_settings['enabled']); - } - // Admin always registers types; menu follows bbs_s (zibll: show when forum on). - $forum_show_menu = is_admin() ? $forum_enabled : false; - $names = !empty($forum_settings['names']) ? $forum_settings['names'] : []; - $forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt'))); - $posts_label = sanitize_text_field((string) ($names['posts'] ?? __('帖子', 'aksrt'))); - $topic_label = sanitize_text_field((string) ($names['topic'] ?? __('话题', 'aksrt'))); - $tag_label = sanitize_text_field((string) ($names['tag'] ?? __('标签', 'aksrt'))); - // Legacy board taxonomy: keep rewrite/bridge, hide from admin (plate CPT is canonical). - register_taxonomy('aksrt_forum_board', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => __('论坛版块', 'aksrt'), - 'singular_name' => __('版块', 'aksrt'), - 'search_items' => __('搜索版块', 'aksrt'), - 'all_items' => __('所有版块', 'aksrt'), - 'edit_item' => __('编辑版块', 'aksrt'), - 'add_new_item' => __('添加版块', 'aksrt'), - ], - 'hierarchical' => true, - 'public' => true, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_rest' => true, - 'show_admin_column' => false, - 'rewrite' => ['slug' => $plate_slug], - ]); - - // Forum Tag taxonomy - register_taxonomy('aksrt_forum_tag', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: forum name */ __('%s标签', 'aksrt'), $forum_label), - 'singular_name' => $tag_label, - 'search_items' => sprintf(/* translators: %s: tag name */ __('搜索%s', 'aksrt'), $tag_label), - 'all_items' => sprintf(/* translators: %s: tag name */ __('所有%s', 'aksrt'), $tag_label), - 'edit_item' => sprintf(/* translators: %s: tag name */ __('编辑%s', 'aksrt'), $tag_label), - 'add_new_item' => sprintf(/* translators: %s: tag name */ __('添加%s', 'aksrt'), $tag_label), - 'menu_name' => $tag_label, - ], - 'hierarchical' => false, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'forum-tag'], - ]); - - // Forum Topic taxonomy (话题,单选,区别于标签) - register_taxonomy('aksrt_forum_topic_cat', 'aksrt_forum_topic', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: forum name */ __('%s话题', 'aksrt'), $forum_label), - 'singular_name' => $topic_label, - 'search_items' => sprintf(/* translators: %s: topic name */ __('搜索%s', 'aksrt'), $topic_label), - 'all_items' => sprintf(/* translators: %s: topic name */ __('所有%s', 'aksrt'), $topic_label), - 'edit_item' => sprintf(/* translators: %s: topic name */ __('编辑%s', 'aksrt'), $topic_label), - 'add_new_item' => sprintf(/* translators: %s: topic name */ __('添加%s', 'aksrt'), $topic_label), - 'menu_name' => $topic_label, - ], - 'hierarchical' => false, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'forum-topic'], - ]); - - // Forum Topic CPT — native sidebar (zibll: forum_post @ pos 40) - register_post_type('aksrt_forum_topic', [ - 'labels' => [ - 'name' => $forum_label . $posts_label, - 'singular_name' => $posts_label, - 'add_new' => sprintf(/* translators: %s: posts name */ __('添加%s', 'aksrt'), $posts_label), - 'add_new_item' => sprintf(/* translators: %s: posts name */ __('发布新%s', 'aksrt'), $posts_label), - 'edit_item' => sprintf(/* translators: %s: posts name */ __('编辑%s', 'aksrt'), $posts_label), - 'view_item' => sprintf(/* translators: %s: posts name */ __('查看%s', 'aksrt'), $posts_label), - 'all_items' => sprintf(/* translators: %s: posts name */ __('所有%s', 'aksrt'), $posts_label), - 'menu_name' => $forum_label . $posts_label, - ], - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'has_archive' => true, - 'menu_position' => 40, - 'menu_icon' => 'dashicons-buddicons-topics', - 'supports' => ['title', 'editor', 'author', 'comments', 'thumbnail', 'excerpt'], - 'capability_type' => 'post', - 'rewrite' => ['slug' => $topic_slug], - ]); -} -add_action('init', 'aksrt_register_forum_cpt'); - -// ============================================================ -// 注册板块 CPT (aksrt_forum_plate) + 板块分区 taxonomy (aksrt_plate_cat) -// §7.0 plate CPT 脚手架:板块从 taxonomy 迁移为独立 CPT, -// 支持作者(超级版主)、正文/摘要/封面、层级父子、独立 meta 扩展位。 -// ============================================================ - -function aksrt_register_forum_plate_cpt() { - if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - if (empty($forum['enabled'])) { - return; - } - } - - $plate_slug = 'board'; - $forum_enabled = true; - $forum = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $plate_slug = !empty($forum['other']['plate_slug']) ? $forum['other']['plate_slug'] : 'board'; - $forum_enabled = !empty($forum['enabled']); - } - $forum_show_menu = is_admin() ? $forum_enabled : false; - $names = !empty($forum['names']) ? $forum['names'] : []; - $forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt'))); - $plate_label = sanitize_text_field((string) ($names['plate'] ?? __('版块', 'aksrt'))); - - // Plate category taxonomy (分区) - register_taxonomy('aksrt_plate_cat', 'aksrt_forum_plate', [ - 'labels' => [ - 'name' => sprintf(/* translators: %s: plate name */ __('%s分区', 'aksrt'), $plate_label), - 'singular_name' => __('分区', 'aksrt'), - 'search_items' => __('搜索分区', 'aksrt'), - 'all_items' => __('所有分区', 'aksrt'), - 'edit_item' => __('编辑分区', 'aksrt'), - 'add_new_item' => __('添加分区', 'aksrt'), - 'menu_name' => __('分区', 'aksrt'), - ], - 'hierarchical' => true, - 'public' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'plate-cat'], - ]); - - // Plate CPT (板块) — native sidebar (zibll: plate @ pos 40) - register_post_type('aksrt_forum_plate', [ - 'labels' => [ - 'name' => $forum_label . $plate_label, - 'singular_name' => $plate_label, - 'add_new' => sprintf(/* translators: %s: plate name */ __('添加%s', 'aksrt'), $plate_label), - 'add_new_item' => sprintf(/* translators: %s: plate name */ __('创建%s', 'aksrt'), $plate_label), - 'edit_item' => sprintf(/* translators: %s: plate name */ __('编辑%s', 'aksrt'), $plate_label), - 'view_item' => sprintf(/* translators: %s: plate name */ __('查看%s', 'aksrt'), $plate_label), - 'search_items' => sprintf(/* translators: %s: plate name */ __('搜索%s', 'aksrt'), $plate_label), - 'not_found' => sprintf(/* translators: %s: plate name */ __('未找到%s', 'aksrt'), $plate_label), - 'not_found_in_trash' => sprintf(/* translators: %s: plate name */ __('回收站中未找到%s', 'aksrt'), $plate_label), - 'all_items' => sprintf(/* translators: %s: plate name */ __('所有%s', 'aksrt'), $plate_label), - 'menu_name' => $forum_label . $plate_label, - ], - 'public' => true, - 'hierarchical' => true, - 'show_ui' => true, - 'show_in_menu' => $forum_show_menu, - 'show_in_rest' => true, - 'has_archive' => false, - 'menu_position' => 40, - 'menu_icon' => 'dashicons-buddicons-forums', - 'supports' => ['title', 'editor', 'author', 'excerpt', 'thumbnail', 'page-attributes'], - 'capability_type' => 'post', - 'rewrite' => ['slug' => $plate_slug, 'with_front' => false], - ]); - - // Keep aksrt_forum_topic assignable to plates via taxonomy bridge - register_taxonomy_for_object_type('aksrt_forum_board', 'aksrt_forum_topic'); -} -add_action('init', 'aksrt_register_forum_plate_cpt'); - -// ============================================================ -// §7.0 forum_post CPT 对齐:注册帖子 meta 扩展位地基 -// 为 aksrt_forum_topic 注册全部 meta key,提供类型安全 + REST 暴露。 -// ============================================================ - -function aksrt_register_forum_topic_meta() { - $post_type = 'aksrt_forum_topic'; - - $meta_fields = [ - // 基础治理 meta (已有,补注册) - '_aksrt_visibility' => ['type' => 'string', 'default' => 'public'], - '_aksrt_review_status' => ['type' => 'string', 'default' => ''], - '_aksrt_sticky' => ['type' => 'integer', 'default' => 0], - '_aksrt_featured' => ['type' => 'integer', 'default' => 0], - '_aksrt_locked' => ['type' => 'integer', 'default' => 0], - '_aksrt_highlight' => ['type' => 'string', 'default' => ''], - '_aksrt_views' => ['type' => 'integer', 'default' => 0], - '_aksrt_moderation_note' => ['type' => 'string', 'default' => ''], - '_aksrt_moderated_by' => ['type' => 'integer', 'default' => 0], - '_aksrt_moderated_at' => ['type' => 'string', 'default' => ''], - - // 帖子类型 + 问答状态 (Wave3) - '_aksrt_bbs_type' => ['type' => 'string', 'default' => 'standard'], - '_aksrt_question_status' => ['type' => 'integer', 'default' => 0], - '_aksrt_adopted_comment' => ['type' => 'integer', 'default' => 0], - - // 投票 (已有,补注册) - '_aksrt_vote_enabled' => ['type' => 'integer', 'default' => 0], - '_aksrt_vote_title' => ['type' => 'string', 'default' => ''], - '_aksrt_vote_multiple' => ['type' => 'integer', 'default' => 0], - '_aksrt_vote_type' => ['type' => 'string', 'default' => 'standard'], - '_aksrt_vote_expiry_days' => ['type' => 'integer', 'default' => 0], - - // 评分 (Wave3) - '_aksrt_score' => ['type' => 'integer', 'default' => 0], - '_aksrt_recommend_score' => ['type' => 'number', 'default' => 0], - '_aksrt_hot_rewarded' => ['type' => 'integer', 'default' => 0], - - // 收藏 (Wave3) - '_aksrt_favorite_count' => ['type' => 'integer', 'default' => 0], - - // 封面扩展位(论坛帖子封面;前台 forum-topic 渲染) - '_aksrt_cover_type' => ['type' => 'string', 'default' => ''], - '_aksrt_cover_image' => ['type' => 'string', 'default' => ''], - '_aksrt_featured_slide' => ['type' => 'array', 'default' => []], - '_aksrt_featured_video' => ['type' => 'string', 'default' => ''], - '_aksrt_cover_ratio' => ['type' => 'string', 'default' => '16:9'], - - // 付费内容扩展位(论坛帖子付费门槛;前台 forum-topic 渲染) - '_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'], - '_aksrt_pay_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy single VIP - '_aksrt_pay_vip1_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_vip2_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_credits' => ['type' => 'integer', 'default' => 0], // legacy alias of credits_cost - '_aksrt_credits_cost' => ['type' => 'integer', 'default' => 0], // runtime unlock price - '_aksrt_credits_vip1' => ['type' => 'integer', 'default' => 0], - '_aksrt_credits_vip2' => ['type' => 'integer', 'default' => 0], - '_aksrt_pay_hide_part' => ['type' => 'integer', 'default' => 0], - '_aksrt_virtual_sales' => ['type' => 'integer', 'default' => 0], - '_aksrt_topic_coupon_s' => ['type' => 'integer', 'default' => 0], - '_aksrt_visibility_roles' => ['type' => 'array', 'default' => []], - - // 状态工作流扩展位 (Wave4) - '_aksrt_closed' => ['type' => 'integer', 'default' => 0], - '_aksrt_hidden' => ['type' => 'integer', 'default' => 0], - - // 板块 CPT 关联 (§7.0 新增) - '_aksrt_plate_id' => ['type' => 'integer', 'default' => 0], - ]; - - foreach ($meta_fields as $key => $args) { - $schema = [ - 'type' => $args['type'], - 'description' => $key, - 'single' => true, - 'default' => $args['default'], - 'show_in_rest' => false, - ]; - - if ($args['type'] === 'array') { - $schema['show_in_rest'] = false; - $schema['type'] = 'array'; - } - - register_post_meta($post_type, $key, $schema); - } - - // Plate CPT meta registration - $plate_meta = [ - '_aksrt_sort_order' => ['type' => 'integer', 'default' => 0], - '_aksrt_icon' => ['type' => 'string', 'default' => ''], - '_aksrt_moderators' => ['type' => 'array', 'default' => []], - '_aksrt_moderator_roles' => ['type' => 'array', 'default' => []], - '_aksrt_posts_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_reply_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_today_reply_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_plate_views' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_count' => ['type' => 'integer', 'default' => 0], - '_aksrt_last_reply' => ['type' => 'string', 'default' => ''], - '_aksrt_last_post' => ['type' => 'string', 'default' => ''], - '_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'], - '_aksrt_add_limit' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay_modo' => ['type' => 'string', 'default' => 'money'], - '_aksrt_follow_pay_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_points' => ['type' => 'integer', 'default' => 0], - '_aksrt_follow_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy read fallback → vip1 - '_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0], - '_aksrt_follow_pay_coupon_s' => ['type' => 'integer', 'default' => 0], - ]; - - foreach ($plate_meta as $key => $args) { - register_post_meta('aksrt_forum_plate', $key, [ - 'type' => $args['type'] === 'array' ? 'array' : $args['type'], - 'description' => $key, - 'single' => true, - 'default' => $args['default'], - 'show_in_rest' => false, - ]); - } -} -add_action('init', 'aksrt_register_forum_topic_meta'); - -// Include forum topics in frontend search results (§7.10). -add_action('pre_get_posts', function ($query) { - if (!is_admin() && $query->is_main_query() && $query->is_search()) { - $post_types = $query->get('post_type'); - if (empty($post_types) || $post_types === 'post') { - $query->set('post_type', ['post', 'page', 'aksrt_forum_topic']); - } elseif (is_array($post_types) && !in_array('aksrt_forum_topic', $post_types, true)) { - $post_types[] = 'aksrt_forum_topic'; - $query->set('post_type', $post_types); - } - } -}); - // ============================================================ \ No newline at end of file diff --git a/inc/helpers/aksrt-credits.php b/inc/helpers/aksrt-credits.php index 3216aa2c..a74c33fc 100644 --- a/inc/helpers/aksrt-credits.php +++ b/inc/helpers/aksrt-credits.php @@ -47,8 +47,6 @@ if (!function_exists('aksrt_credits_label')) { 'download' => __('下载消费', 'aksrt'), 'admin' => __('管理员调整', 'aksrt'), 'refund' => __('退款', 'aksrt'), - 'forum_topic' => __('论坛发帖', 'aksrt'), - 'forum_reply' => __('论坛回复', 'aksrt'), 'free_sign_up' => __('注册奖励', 'aksrt'), 'free_sign_in' => __('每日签到', 'aksrt'), 'free_login' => __('每日登录', 'aksrt'), @@ -58,13 +56,6 @@ if (!function_exists('aksrt_credits_label')) { 'free_post_favorite'=> __('文章被收藏', 'aksrt'), 'free_comment_new'=> __('发表评论', 'aksrt'), 'free_comment_like'=> __('评论被赞', 'aksrt'), - 'free_bbs_posts_new'=> __('论坛发帖', 'aksrt'), - 'free_bbs_reply'=> __('论坛回复', 'aksrt'), - 'free_bbs_essence'=> __('帖子加精', 'aksrt'), - 'free_bbs_adopt'=> __('回复被采纳', 'aksrt'), - 'free_bbs_posts_hot'=> __('热帖奖励', 'aksrt'), - 'free_bbs_comment_hot'=> __('神评奖励', 'aksrt'), - 'free_bbs_score_extra'=> __('帖子加分', 'aksrt'), ]; if (isset($map[$reason])) { return $map[$reason]; diff --git a/inc/helpers/aksrt-forum-widgets.php b/inc/helpers/aksrt-forum-widgets.php deleted file mode 100644 index 49ff9e22..00000000 --- a/inc/helpers/aksrt-forum-widgets.php +++ /dev/null @@ -1,363 +0,0 @@ - (string) get_post_meta($post_id, '_aksrt_topping', true) === '1' - || (string) get_post_meta($post_id, '_aksrt_is_top', true) === '1', - 'essence' => (string) get_post_meta($post_id, '_aksrt_essence', true) === '1', - 'views' => (int) get_post_meta($post_id, '_aksrt_views', true), - 'replies' => (int) get_comments_number($post_id), - 'plate_id' => (int) get_post_meta($post_id, '_aksrt_plate_id', true), - ]; - } -endif; - -if (!function_exists('aksrt_forum_topic_badges_html')) : - /** - * Topping / essence badges for forum topic rows. - */ - function aksrt_forum_topic_badges_html(int $post_id, bool $show_topping = true): string { - $flags = aksrt_forum_topic_meta_flags($post_id); - $html = ''; - if ($show_topping && !empty($flags['topping'])) { - $html .= '' . esc_html__('置顶', 'aksrt') . ''; - } - if (!empty($flags['essence'])) { - $html .= '' . esc_html__('精华', 'aksrt') . ''; - } - return $html; - } -endif; - -if (!function_exists('aksrt_render_forum_topic_mini')) : - /** - * Mini forum topic row (zibll zib_bbs_get_posts_mini_list parity). - * - * @param int|null $post_id Null = current post in loop. - * @param string $class Extra classes on outer element. - * @param bool $show_topping Whether to show topping badge. - */ - function aksrt_render_forum_topic_mini(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string { - $post_id = $post_id ?: (int) get_the_ID(); - if ($post_id <= 0) { - return ''; - } - $post = get_post($post_id); - if (!$post || $post->post_status !== 'publish') { - return ''; - } - - $author_id = (int) $post->post_author; - $flags = aksrt_forum_topic_meta_flags($post_id); - $permalink = get_permalink($post_id); - $title = get_the_title($post_id); - - $avatar_url = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($author_id, ['size' => 48]) - : get_avatar_url($author_id, ['size' => 48]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($author_id) - : get_author_posts_url($author_id); - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $author_id, - 'class' => 'flex ac flex1', - 'follow' => false, - 'medal' => false, - 'level' => false, - ]) - : '' . esc_html(get_the_author_meta('display_name', $author_id)) . ''; - - // Prefer last reply time when available. - $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true); - if ($time_raw === '' || !strtotime($time_raw)) { - $time_raw = (string) $post->post_date; - } - $time_txt = function_exists('aksrt_time_ago') - ? aksrt_time_ago($time_raw) - : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt'); - - $views_txt = function_exists('aksrt_format_number') - ? aksrt_format_number($flags['views']) - : (string) $flags['views']; - - $cls = trim('forum-posts mini aksrt-ftopic-mini relative ' . $class); - $html = ''; - $html .= aksrt_forum_topic_badges_html($post_id, $show_topping); - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= '' . $name . ''; - $html .= '' . esc_html($time_txt) . ''; - $html .= ' ' . esc_html((string) $views_txt) . ''; - if ($flags['replies'] > 0) { - $html .= ' ' . esc_html((string) $flags['replies']) . ''; - } - $html .= '
'; - - return $html; - } -endif; - -if (!function_exists('aksrt_render_forum_topic_detail')) : - /** - * Detail forum topic row (richer entry meta). - * - * @param int|null $post_id - * @param string $class - * @param bool $show_topping - */ - function aksrt_render_forum_topic_detail(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string { - $post_id = $post_id ?: (int) get_the_ID(); - if ($post_id <= 0) { - return ''; - } - $post = get_post($post_id); - if (!$post || $post->post_status !== 'publish') { - return ''; - } - - $author_id = (int) $post->post_author; - $flags = aksrt_forum_topic_meta_flags($post_id); - $permalink = get_permalink($post_id); - $title = get_the_title($post_id); - $excerpt = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 36, '…'); - - $avatar_url = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($author_id, ['size' => 64]) - : get_avatar_url($author_id, ['size' => 64]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($author_id) - : get_author_posts_url($author_id); - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $author_id, - 'class' => 'flex ac', - 'follow' => false, - 'medal' => true, - 'level' => true, - ]) - : '' . esc_html(get_the_author_meta('display_name', $author_id)) . ''; - - $time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true); - if ($time_raw === '' || !strtotime($time_raw)) { - $time_raw = (string) $post->post_date; - } - $time_txt = function_exists('aksrt_time_ago') - ? aksrt_time_ago($time_raw) - : human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt'); - - $views_txt = function_exists('aksrt_format_number') - ? aksrt_format_number($flags['views']) - : (string) $flags['views']; - - // Optional plate name chip. - $plate_chip = ''; - if ($flags['plate_id'] > 0) { - $plate = get_post($flags['plate_id']); - if ($plate instanceof WP_Post) { - $plate_chip = '' . esc_html(get_the_title($plate)) . ''; - } - } - - $cls = trim('forum-posts detail aksrt-ftopic-detail relative ' . $class); - $html = ''; - $html .= aksrt_forum_topic_badges_html($post_id, $show_topping); - $html .= '
'; - $html .= ''; - $html .= '
' . $name . '
' . esc_html($time_txt) . '
'; - $html .= '
'; - $html .= '

' . esc_html($title) . '

'; - if ($excerpt !== '') { - $html .= '
' . esc_html($excerpt) . '
'; - } - $html .= '
'; - $html .= $plate_chip; - $html .= ' ' . esc_html((string) $views_txt) . ''; - $html .= ' ' . esc_html((string) $flags['replies']) . ''; - $html .= '
'; - - return $html; - } -endif; - -if (!function_exists('aksrt_render_plate_thumb_html')) : - /** - * Shared plate thumb with lazy pattern. - */ - function aksrt_render_plate_thumb_html(int $plate_id, string $size = 'thumbnail'): string { - $thumb = get_the_post_thumbnail_url($plate_id, $size); - $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - if ($thumb) { - return ''; - } - if ($icon !== '') { - return ''; - } - return ''; - } -endif; - -if (!function_exists('aksrt_render_plate_row')) : - /** - * Plate list row (zibll plate-item flex). - * - * @param WP_Post|int $plate - */ - function aksrt_render_plate_row($plate): string { - $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate); - if (!$post instanceof WP_Post) { - return ''; - } - $id = (int) $post->ID; - $url = get_permalink($id); - $count = (int) get_post_meta($id, '_aksrt_posts_count', true); - $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); - $desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 18); - - $html = ''; - $html .= aksrt_render_plate_thumb_html($id, 'thumbnail'); - $html .= '
'; - $html .= '

' . esc_html(get_the_title($post)) . '

'; - if ($desc !== '') { - $html .= '
' . esc_html($desc) . '
'; - } - $html .= '
'; - $html .= '' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . ''; - if ($follow > 0) { - $html .= '' . esc_html(sprintf(/* translators: %d follow count */ __('%d 关注', 'aksrt'), $follow)) . ''; - } - $html .= '
'; - return $html; - } -endif; - -if (!function_exists('aksrt_render_plate_card')) : - /** - * Plate card slide item. - * - * @param WP_Post|int $plate - */ - function aksrt_render_plate_card($plate): string { - $post = $plate instanceof WP_Post ? $plate : get_post((int) $plate); - if (!$post instanceof WP_Post) { - return ''; - } - $id = (int) $post->ID; - $url = get_permalink($id); - $thumb = get_the_post_thumbnail_url($id, 'medium'); - $count = (int) get_post_meta($id, '_aksrt_posts_count', true); - $follow = (int) get_post_meta($id, '_aksrt_follow_count', true); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - - $html = ''; - $html .= '
'; - if ($thumb) { - $html .= ''; - } else { - $html .= ''; - } - $html .= '
'; - $html .= '
' . esc_html(get_the_title($post)) . '
'; - $html .= '
'; - $html .= '' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . ''; - $html .= '' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . ''; - $html .= '
'; - return $html; - } -endif; - -if (!function_exists('aksrt_render_moderator_row')) : - /** - * Moderator identity row with optional follow. - * - * @param int $user_id - * @param string $role author|super|moderator - * @param bool $show_avatar - * @param string $style list|grid - */ - function aksrt_render_moderator_row(int $user_id, string $role = 'moderator', bool $show_avatar = true, string $style = 'list'): string { - if ($user_id <= 0) { - return ''; - } - $user = get_userdata($user_id); - if (!$user) { - return ''; - } - $avatar = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($user_id, ['size' => 64]) - : get_avatar_url($user_id, ['size' => 64]); - $home = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($user_id) - : get_author_posts_url($user_id); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - - if ($role === 'author' || $role === 'super') { - $role_label = __('超级版主', 'aksrt'); - } else { - $role_label = __('版主', 'aksrt'); - } - - $name = function_exists('aksrt_get_user_name') - ? aksrt_get_user_name([ - 'id' => $user_id, - 'class' => 'flex ac flex1', - 'follow' => false, - 'medal' => true, - 'level' => true, - ]) - : '' . esc_html($user->display_name) . ''; - - // Follow control (not for self). - $follow_html = ''; - $cuid = get_current_user_id(); - if ($cuid > 0 && $cuid !== $user_id && class_exists('\AKSRT\User\Follow')) { - $following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $user_id); - $label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt'); - $follow_html = ''; - } - - if ($style === 'grid') { - $html = '
'; - if ($show_avatar) { - $html .= ''; - } - $html .= '
' . $name . '
'; - $html .= '
' . esc_html($role_label) . '
'; - $html .= $follow_html; - $html .= '
'; - return $html; - } - - $html = '
'; - if ($show_avatar) { - $html .= ''; - } - $html .= '
'; - $html .= '
' . $name . '
'; - $html .= '
' . esc_html($role_label) . '
'; - $html .= '
'; - $html .= $follow_html; - $html .= '
'; - return $html; - } -endif; diff --git a/inc/helpers/aksrt-forum.php b/inc/helpers/aksrt-forum.php index e33e7883..1bb49709 100644 --- a/inc/helpers/aksrt-forum.php +++ b/inc/helpers/aksrt-forum.php @@ -1,8 +1,9 @@ $args Optional query args (board, plate, …). - */ - function aksrt_get_forum_compose_url(array $args = []): string { - $url = aksrt_get_page_url_by_template('templates/forum/forum-compose.php'); - if ($url === '') { - $url = home_url('/'); - } - - // Normalize plate → board for compose form contract. - if (isset($args['plate']) && !isset($args['board'])) { - $args['board'] = $args['plate']; - unset($args['plate']); - } - - $clean = []; - foreach ($args as $key => $value) { - $key = sanitize_key((string) $key); - if ($key === '') { - continue; - } - if (is_bool($value)) { - $clean[$key] = $value ? '1' : '0'; - } else { - $clean[$key] = (string) $value; - } - } - - return !empty($clean) ? add_query_arg($clean, $url) : $url; - } -endif; diff --git a/inc/helpers/aksrt-header-menu-buttons.php b/inc/helpers/aksrt-header-menu-buttons.php index 8a0236c0..a6cad02c 100644 --- a/inc/helpers/aksrt-header-menu-buttons.php +++ b/inc/helpers/aksrt-header-menu-buttons.php @@ -21,17 +21,6 @@ if (!function_exists('aksrt_get_header_newposts_config')) : $types = is_array($cfg['types'] ?? null) ? $cfg['types'] : []; if (!$types) { $types = ['post']; - if (class_exists('\AKSRT\Settings\Settings')) { - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - if (!empty($forum['enabled'])) { - $types[] = 'topic'; - $types[] = 'plate'; - } - if (!empty($shop['enabled'])) { - $types[] = 'product'; - } - } } $cfg['types'] = $types; if (empty($cfg['class'])) { @@ -65,53 +54,6 @@ if (!function_exists('aksrt_get_new_add_actions')) : 'label' => __('写文章', 'aksrt'), 'icon' => 'fas fa-file-alt', ]; - } elseif ($type === 'topic') { - $url = function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : ''; - if ($url === '') { - continue; - } - $out[] = [ - 'type' => 'topic', - 'url' => $url, - 'label' => __('发帖', 'aksrt'), - 'icon' => 'fas fa-comments', - ]; - } elseif ($type === 'product') { - $url = ''; - if (function_exists('aksrt_get_product_edit_url')) { - $url = (string) aksrt_get_product_edit_url(); - } - if ($url === '') { - $url = admin_url('post-new.php?post_type=aksrt_product'); - } - $out[] = [ - 'type' => 'product', - 'url' => $url, - 'label' => __('发布商品', 'aksrt'), - 'icon' => 'fas fa-shopping-bag', - ]; - } elseif ($type === 'plate') { - // zibll bbs_plate → AKSRT forum plate CPT (admin create for entitled users). - $can = current_user_can('manage_options'); - if (!$can && class_exists('\AKSRT\Forum\Forum')) { - try { - $forum = \AKSRT\Forum\Forum::get_instance(); - if (method_exists($forum, 'forum_user_can')) { - $can = (bool) $forum->forum_user_can(get_current_user_id(), 'plate_create'); - } - } catch (\Throwable $e) { - $can = false; - } - } - if (!$can || !post_type_exists('aksrt_forum_plate')) { - continue; - } - $out[] = [ - 'type' => 'plate', - 'url' => admin_url('post-new.php?post_type=aksrt_forum_plate'), - 'label' => __('创建版块', 'aksrt'), - 'icon' => 'fas fa-th-large', - ]; } } return $out; diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 5d0531fa..adcb9ec0 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -356,26 +356,7 @@ if (!function_exists('aksrt_content_order_cookie_token_key')) { } /** - * Whether guest cash purchase is allowed for forum topic / plate follow. - * Reuses global pay_no_logged_in (same switch as content). - * - * @param WP_Post|int|null $post Topic or plate post (optional filter context). - */ -if (!function_exists('aksrt_allow_guest_forum_pay')) { - function aksrt_allow_guest_forum_pay($post = null): bool { - $order = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_order_settings() : []; - $paid = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_paid_content_settings() : []; - $allow = !empty($order['no_logged_in']) || !empty($paid['no_logged_in']); - /** - * @param bool $allow - * @param WP_Post|int|null $post - */ - return (bool) apply_filters('aksrt_allow_guest_forum_pay', $allow, $post); - } -} - -/** - * Set guest paid cookies after cash order paid (content / topic / plate_follow). + * Set guest paid cookies after cash order paid (content). * * @param array $order */ @@ -495,18 +476,6 @@ if (!function_exists('aksrt_guest_has_paid_access')) { } } -/** - * Guest paid plate follow access (cookie). - */ -if (!function_exists('aksrt_guest_has_plate_follow_access')) { - function aksrt_guest_has_plate_follow_access(int $plate_id): bool { - $type = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW - : 'plate_follow'; - return null !== aksrt_get_guest_paid_order($plate_id, $type); - } -} - /** * Resolve the amount a given user should pay for a post (Zibll VIP tier min). * diff --git a/inc/helpers/aksrt-runtime-win.php b/inc/helpers/aksrt-runtime-win.php index 18c5595d..ab0e7dcf 100644 --- a/inc/helpers/aksrt-runtime-win.php +++ b/inc/helpers/aksrt-runtime-win.php @@ -7,51 +7,6 @@ defined('ABSPATH') || exit; -if (!function_exists('aksrt_get_float_shop_cart_html')) : - /** - * Floating shop cart button with badge. - */ - function aksrt_get_float_shop_cart_html(array $item = []): string { - if (!is_user_logged_in()) { - return ''; - } - $shop_on = true; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_on = !empty($shop['enabled']); - } - if (!$shop_on) { - return ''; - } - // Skip on cart page. - if (function_exists('is_page_template') && is_page_template('templates/pages/page-cart.php')) { - return ''; - } - $url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; - if ($url === '') { - return ''; - } - $count = 0; - if (class_exists('\AKSRT\Shop\Shop')) { - $count = (int) \AKSRT\Shop\Shop::get_instance()->get_cart_count(get_current_user_id()); - } - $icon = !empty($item['icon']) ? (string) $item['icon'] : 'fas fa-shopping-cart'; - $style = []; - if (!empty($item['color'])) { - $style[] = '--this-color:' . esc_attr((string) $item['color']); - } - if (!empty($item['bg'])) { - $style[] = '--this-bg:' . esc_attr((string) $item['bg']); - } - $style_attr = $style ? ' style="' . implode(';', $style) . '"' : ''; - $badge = $count > 0 ? (string) min(99, $count) : ''; - return '' - . '' - . '' . esc_html($badge) . '' - . ''; - } -endif; - if (!function_exists('aksrt_get_float_pay_vip_html')) : /** * Floating VIP open button. diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php index a071718d..a0f67678 100644 --- a/inc/helpers/aksrt-user-center.php +++ b/inc/helpers/aksrt-user-center.php @@ -485,18 +485,8 @@ if (!function_exists('aksrt_get_user_center_render_context')) { $wallet_balance = (float) ($dashboard['wallet_balance'] ?? 0.0); $runtime_config = aksrt_user_center_runtime_config($user_id, $tabs['default'], $active_tab, $tabs['urls']); $runtime_config['avatarUpload'] = aksrt_user_center_avatar_upload_config($allow_avatar_upload); - $shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $forum_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_forum_settings() : []; $revenue_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_revenue_settings() : []; - $shop_enabled = !empty($shop_settings['enabled']); - $forum_enabled = !empty($forum_settings['enabled']); $new_post_url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : ''; - $new_product_url = $shop_enabled && function_exists('aksrt_get_page_url_by_template') - ? aksrt_get_page_url_by_template('templates/pages/page-product-edit.php') - : ''; - $forum_compose_url = $forum_enabled && function_exists('aksrt_get_page_url_by_template') - ? aksrt_get_page_url_by_template('templates/forum/forum-compose.php') - : ''; $revenue_url = (!empty($revenue_settings['enabled']) || !empty($affiliate_settings['enabled'])) && function_exists('aksrt_get_revenue_page_url') ? aksrt_get_revenue_page_url() : ''; @@ -523,8 +513,6 @@ if (!function_exists('aksrt_get_user_center_render_context')) { 'notification_unread_count' => function_exists('aksrt_get_user_unread_notification_count') ? aksrt_get_user_unread_notification_count($user_id) : 0, 'messages_url' => $messages_enabled && function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '', 'new_post_url' => $new_post_url, - 'new_product_url' => $new_product_url, - 'forum_compose_url' => $forum_compose_url, 'revenue_url' => $revenue_url, 'per_page' => 10, 'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1), diff --git a/inc/helpers/aksrt-user-home.php b/inc/helpers/aksrt-user-home.php index 0330a41a..49c2fb25 100644 --- a/inc/helpers/aksrt-user-home.php +++ b/inc/helpers/aksrt-user-home.php @@ -79,52 +79,16 @@ if (!function_exists('aksrt_get_user_home_page_context')) { $display_tabs = $is_mobile ? $enabled_tabs_mobile : $enabled_tabs_pc; $default_tab = !empty($display_tabs) ? $display_tabs[0] : $enabled_tabs[0]; - $forum_instance = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null; - $forum_stats = $forum_instance ? $forum_instance->get_user_forum_stats($user_id) : []; - $forum_topic_count = (int) ($forum_stats['topic_count'] ?? 0); - $product_count = 0; - if (class_exists('\AKSRT\Shop\Shop')) { - $product_count = (int) \AKSRT\Shop\Shop::get_instance()->count_author_products($user_id); - } - $follow_count = 0; if (class_exists('\AKSRT\User\Follow')) { $follow_count = (int) \AKSRT\User\Follow::get_instance()->get_follower_count($user_id); } - $plate_count = 0; - if ($forum_instance) { - $seen_plates = []; - if (method_exists($forum_instance, 'get_user_followed_plates')) { - foreach ((array) $forum_instance->get_user_followed_plates($user_id) as $plate) { - $pid = (int) ($plate['id'] ?? 0); - if ($pid > 0) { - $seen_plates[$pid] = true; - } - } - } - if (method_exists($forum_instance, 'get_user_managed_plates')) { - $managed = (array) $forum_instance->get_user_managed_plates($user_id); - foreach (['owned', 'moderating'] as $bucket) { - foreach ((array) ($managed[$bucket] ?? []) as $plate) { - $pid = (int) ($plate['id'] ?? 0); - if ($pid > 0) { - $seen_plates[$pid] = true; - } - } - } - } - $plate_count = count($seen_plates); - } - $tab_counts = [ 'posts' => $post_count, 'comments' => $comment_count, 'favorites' => $favorite_count, 'credits' => $credits_enabled && $is_own_profile ? (int) round((float) $credit_balance) : 0, - 'product' => $product_count, - 'plate' => $plate_count, - 'forum' => $forum_topic_count, 'follow' => $follow_count, ]; @@ -133,9 +97,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) { 'comments' => __('评论', 'aksrt'), 'favorites' => __('收藏', 'aksrt'), 'credits' => sprintf(__('我的%s', 'aksrt'), $currency_name), - 'product' => __('商品', 'aksrt'), - 'plate' => __('圈子', 'aksrt'), - 'forum' => __('论坛', 'aksrt'), 'follow' => __('粉丝', 'aksrt'), // Zibll: 粉丝 ]; foreach ($visible_tabs as $tab_key => $tab_config) { @@ -174,9 +135,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) { 'active_tab' => $active_tab, 'tab_counts' => $tab_counts, 'tab_labels' => $tab_labels, - 'forum_instance' => $forum_instance, - 'forum_stats' => $forum_stats, - 'forum_topic_count' => $forum_topic_count, 'home_url' => $home_url, 'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1), 'comments_page' => max(1, isset($_GET['comments_page']) ? (int) $_GET['comments_page'] : 1), diff --git a/inc/helpers/header/aksrt-header-navbar.php b/inc/helpers/header/aksrt-header-navbar.php index ccc50245..2ef71fce 100644 --- a/inc/helpers/header/aksrt-header-navbar.php +++ b/inc/helpers/header/aksrt-header-navbar.php @@ -208,19 +208,7 @@ if (!function_exists('aksrt_header_user_box_html')) : if ($pub_text === '') { $pub_text = __('发布内容', 'aksrt'); } - $pub_type = sanitize_key((string) ($all['header_user_newposts_btn'] ?? 'post')); $pub_url = (string) $ctx['publish_url']; - if ($pub_type === 'topic' && function_exists('aksrt_get_forum_compose_url')) { - $u = aksrt_get_forum_compose_url(); - if ($u) { $pub_url = $u; } - } elseif ($pub_type === 'product') { - if (function_exists('aksrt_get_product_edit_url')) { - $u = (string) aksrt_get_product_edit_url(); - if ($u !== '') { $pub_url = $u; } - } else { - $pub_url = admin_url('post-new.php?post_type=aksrt_product'); - } - } $href .= ''; $href .= '
'; $href .= '
' . esc_html($pub_text) . '
'; @@ -240,13 +228,6 @@ if (!function_exists('aksrt_header_user_box_html')) : $href .= ''; $href .= '
'; $href .= '
' . esc_html__('模块布局', 'aksrt') . '
'; - // zibll: 商城中心. - $shop_url = post_type_exists('aksrt_product') - ? admin_url('edit.php?post_type=aksrt_product') - : admin_url('admin.php?page=aksrt_pay'); - $href .= ''; - $href .= '
'; - $href .= '
' . esc_html__('商城中心', 'aksrt') . '
'; $href .= ''; $href .= '
'; $href .= '
' . esc_html__('后台管理', 'aksrt') . '
'; diff --git a/inc/helpers/homepage/header-components.php b/inc/helpers/homepage/header-components.php index e9fd63d3..e9db142c 100644 --- a/inc/helpers/homepage/header-components.php +++ b/inc/helpers/homepage/header-components.php @@ -22,9 +22,6 @@ if (!function_exists('aksrt_header_components_page_type_keys')) { 'author', 'single', 'search', - 'forum_home', - 'forum_plate', - 'forum_post', 'page', ]; } @@ -46,7 +43,6 @@ if (!function_exists('aksrt_header_slide_excluded_page_templates')) { 'templates/pages/page-verify-email.php', 'templates/pages/page-download.php', 'templates/pages/page-download-file.php', - 'templates/pages/page-forums.php', ]; } } @@ -77,18 +73,6 @@ if (!function_exists('aksrt_header_slide_match_page_type')) { return is_tax('topics'); } - if ($type === 'forum_home') { - return is_page_template('templates/pages/page-forums.php'); - } - - if ($type === 'forum_plate') { - return is_singular('aksrt_forum_plate') || $post_type === 'aksrt_forum_plate'; - } - - if ($type === 'forum_post') { - return is_singular('aksrt_forum_topic') || $post_type === 'aksrt_forum_topic'; - } - // Generic WP_Query is_* flags (home/category/tag/author/single/search/page). $query_flag = 'is_' . $type; if (empty($wp_query->{$query_flag})) { diff --git a/inc/helpers/user-center-renderers/credits-points-tasks.php b/inc/helpers/user-center-renderers/credits-points-tasks.php index 2a438f49..00af0571 100644 --- a/inc/helpers/user-center-renderers/credits-points-tasks.php +++ b/inc/helpers/user-center-renderers/credits-points-tasks.php @@ -23,22 +23,6 @@ $aksrt_points_task_action = static function (string $key): array { $submit = (string) aksrt_get_page_url_by_template('templates/pages/page-submit.php'); } - $forum_on = false; - $forum_home = ''; - $forum_compose = ''; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - $forum_on = !empty($f['enabled']); - } - if ($forum_on) { - if (function_exists('aksrt_get_forum_home_url')) { - $forum_home = (string) aksrt_get_forum_home_url(); - } - if (function_exists('aksrt_get_forum_compose_url')) { - $forum_compose = (string) aksrt_get_forum_compose_url(); - } - } - switch ($key) { case 'sign_in': return [ @@ -93,37 +77,10 @@ $aksrt_points_task_action = static function (string $key): array { 'label' => __('去主页', 'aksrt'), 'href' => $center !== '' ? $center : $home, ]; - case 'bbs_posts_new': - case 'bbs_reply': - if (!$forum_on) { + default: return ['type' => 'none', 'label' => '']; - } - $href = $forum_compose !== '' ? $forum_compose : $forum_home; - if ($href === '') { - return ['type' => 'none', 'label' => '']; - } - return [ - 'type' => 'link', - 'label' => __('去完成', 'aksrt'), - 'href' => $href, - ]; - case 'bbs_essence': - case 'bbs_adopt': - case 'bbs_posts_hot': - case 'bbs_comment_hot': - case 'bbs_score_extra': - if (!$forum_on || $forum_home === '') { - return ['type' => 'none', 'label' => '']; - } - return [ - 'type' => 'link', - 'label' => __('去看看', 'aksrt'), - 'href' => $forum_home, - ]; - default: - return ['type' => 'none', 'label' => '']; - } -}; + } + }; $user_id = (int) ($user_id ?? get_current_user_id()); $today = \AKSRT\Credits\PointsFree::today_total($user_id); diff --git a/inc/modules/user-parts/routing.php b/inc/modules/user-parts/routing.php index 27bbdd3c..14e6e815 100644 --- a/inc/modules/user-parts/routing.php +++ b/inc/modules/user-parts/routing.php @@ -16,7 +16,6 @@ add_filter('query_vars', function ($vars) { $vars[] = 'aksrt_user_center'; $vars[] = 'user_nicename'; $vars[] = 'aksrt_orders'; - $vars[] = 'aksrt_cart'; $vars[] = 'aksrt_revenue'; return $vars; }); @@ -40,13 +39,11 @@ add_action('init', function () { add_rewrite_tag('%aksrt_user_center%', '([^&]+)'); add_rewrite_tag('%user_nicename%', '([^/]+)'); add_rewrite_tag('%aksrt_orders%', '([0-1])'); - add_rewrite_tag('%aksrt_cart%', '([0-1])'); add_rewrite_tag('%aksrt_revenue%', '([0-1])'); add_rewrite_rule('^profile/?$', 'index.php?aksrt_user_center=1', 'top'); add_rewrite_rule('^user/([^/]+)/?$', 'index.php?user_nicename=$matches[1]', 'top'); // Commerce center aliases (work even before WP page is assigned). add_rewrite_rule('^orders/?$', 'index.php?aksrt_orders=1', 'top'); - add_rewrite_rule('^cart/?$', 'index.php?aksrt_cart=1', 'top'); add_rewrite_rule('^revenue/?$', 'index.php?aksrt_revenue=1', 'top'); }); diff --git a/inc/modules/user-parts/template-registry.php b/inc/modules/user-parts/template-registry.php index 263f6178..90f5e4d2 100644 --- a/inc/modules/user-parts/template-registry.php +++ b/inc/modules/user-parts/template-registry.php @@ -137,20 +137,6 @@ $_aksrt_special_templates = [ 'title' => '发布文章', 'fallback' => '/submit/', ], - 'product-edit' => [ - 'file' => 'templates/pages/page-product-edit.php', - 'slug' => 'product-edit', - 'title' => '发布商品', - 'fallback' => '/product-edit/', - 'auto_create' => false, - ], - 'forum-compose' => [ - 'file' => 'templates/forum/forum-compose.php', - 'slug' => 'forum-compose', - 'title' => '论坛发帖', - 'fallback' => '/forum-compose/', - 'auto_create' => false, - ], ]; /** @var array 缓存 key=模板文件路径 => value=WP_Post|null */ diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index bbb546e3..ac478d3e 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -84,17 +84,6 @@ function aksrt_get_orders_page_url(): string { } endif; -if (!function_exists('aksrt_get_cart_page_url')) : -/** - * 购物车页面 URL(模板 templates/pages/page-cart.php)。 - */ -function aksrt_get_cart_page_url(): string { - return function_exists('aksrt_get_template_url') - ? aksrt_get_template_url('cart') - : home_url('/cart/'); -} -endif; - if (!function_exists('aksrt_get_revenue_page_url')) : /** * 收益中心页面 URL(模板 templates/pages/page-revenue.php)。 diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index b0d5145f..f3ec4692 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -92,36 +92,6 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetIconCoverCards'); register_widget('\AKSRT\Widgets\WidgetBookmarkLinks'); - - // Forum widgets - if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { - register_widget('\AKSRT\Widgets\WidgetForumBoardList'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumHotTopics'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumRecentTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumRecentTopics'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateLists'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateInfo')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateInfo'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumPlateModerator')) { - register_widget('\AKSRT\Widgets\WidgetForumPlateModerator'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { - register_widget('\AKSRT\Widgets\WidgetForumTopicLists'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTopicTerms')) { - register_widget('\AKSRT\Widgets\WidgetForumTopicTerms'); - } - if (class_exists('\AKSRT\Widgets\WidgetForumTabTopics')) { - register_widget('\AKSRT\Widgets\WidgetForumTabTopics'); - } - // Shop product widgets (§6.9) register_widget('\AKSRT\Widgets\WidgetProductList'); register_widget('\AKSRT\Widgets\WidgetProductOneline'); diff --git a/slim-forum.php b/slim-forum.php deleted file mode 100644 index 736407a7..00000000 --- a/slim-forum.php +++ /dev/null @@ -1,9 +0,0 @@ -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\d+)', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_board'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P\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\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_topic_detail'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/moderate', [ - 'methods' => 'POST', - 'callback' => [$this, 'moderate_topic'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\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[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\d+)/favorite', [ - 'methods' => 'POST', - 'callback' => [$this, 'toggle_favorite'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\d+)/score', [ - 'methods' => 'POST', - 'callback' => [$this, 'score_topic'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P\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\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_plate'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\d+)', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_plate'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P\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\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\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\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\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\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); - } - -} diff --git a/src/AKSRT/Admin/ForumControllerPart1.php b/src/AKSRT/Admin/ForumControllerPart1.php deleted file mode 100644 index 92d57346..00000000 --- a/src/AKSRT/Admin/ForumControllerPart1.php +++ /dev/null @@ -1,390 +0,0 @@ -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\Forum::APPLICATION_PENDING], - ['label' => __('已通过', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_APPROVED], - ['label' => __('已拒绝', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_REJECTED], - ], - ], - ], - ]); - } -} diff --git a/src/AKSRT/Admin/ForumControllerPart2.php b/src/AKSRT/Admin/ForumControllerPart2.php deleted file mode 100644 index e99b9c1f..00000000 --- a/src/AKSRT/Admin/ForumControllerPart2.php +++ /dev/null @@ -1,188 +0,0 @@ -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\Forum { - return class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\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')]); - } -} diff --git a/src/AKSRT/Admin/GlobalControllerPart1.php b/src/AKSRT/Admin/GlobalControllerPart1.php index e1aa6795..53109d7d 100644 --- a/src/AKSRT/Admin/GlobalControllerPart1.php +++ b/src/AKSRT/Admin/GlobalControllerPart1.php @@ -185,7 +185,7 @@ trait GlobalControllerPart1 { } 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']; + $valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add']; $type = sanitize_text_field($item['type'] ?? 'custom'); if (!in_array($type, $valid_types, true)) { $type = 'custom'; diff --git a/src/AKSRT/Admin/Native/PayOrders.php b/src/AKSRT/Admin/Native/PayOrders.php index 338b1e62..166366a6 100644 --- a/src/AKSRT/Admin/Native/PayOrders.php +++ b/src/AKSRT/Admin/Native/PayOrders.php @@ -92,7 +92,6 @@ class PayOrders { 'vip' => __('VIP', 'aksrt'), 'recharge' => __('充值', 'aksrt'), 'shop_product' => __('商城商品', 'aksrt'), - 'plate_follow' => __('版块关注', 'aksrt'), ]; echo '
    '; $tab_bits = []; diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php index 8741fc7f..7d3d9f6b 100644 --- a/src/AKSRT/Admin/Native/UsersMenus.php +++ b/src/AKSRT/Admin/Native/UsersMenus.php @@ -573,7 +573,7 @@ class UsersMenus { } echo '

    '; echo '

    '; diff --git a/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php b/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php index 31792fb4..f305441b 100644 --- a/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php +++ b/src/AKSRT/Admin/Schema/SchemaExtensionsAudit.php @@ -28,7 +28,6 @@ class SchemaExtensionsAudit { ['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' => [ diff --git a/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php b/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php index 95521428..2a9a68f4 100644 --- a/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php +++ b/src/AKSRT/Admin/Schema/SchemaExtensionsSecurity.php @@ -81,9 +81,6 @@ class SchemaExtensionsSecurity { ['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], ]], ], ]; diff --git a/src/AKSRT/Admin/Schema/SchemaForum.php b/src/AKSRT/Admin/Schema/SchemaForum.php deleted file mode 100644 index 834da7d8..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForum.php +++ /dev/null @@ -1,126 +0,0 @@ - 'forum', - 'title' => '社区&论坛', - 'sections' => [ - 'basic' => SchemaForumBasic::build(), - 'names' => SchemaForumNames::build(), - 'homepage' => SchemaForumHomepage::build(), - 'board_page' => SchemaForumBoardPage::build(), - 'post_page' => SchemaForumPostPage::build(), - 'reply' => SchemaForumReply::build(), - 'permissions' => SchemaForumPermissions::build(), - 'upload' => SchemaForumUpload::build(), - 'credits' => SchemaForumCredits::build(), - 'other' => SchemaForumOther::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\Settings')) { - return []; - } - - $defs = \AKSRT\Settings\Settings::cap_definitions(); - $cols = \AKSRT\Settings\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; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumBasic.php b/src/AKSRT/Admin/Schema/SchemaForumBasic.php deleted file mode 100644 index 8e554178..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumBasic.php +++ /dev/null @@ -1,28 +0,0 @@ - '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'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php b/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php deleted file mode 100644 index 6b36f487..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumBoardPage.php +++ /dev/null @@ -1,34 +0,0 @@ - '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], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumCredits.php b/src/AKSRT/Admin/Schema/SchemaForumCredits.php deleted file mode 100644 index 823e021b..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumCredits.php +++ /dev/null @@ -1,34 +0,0 @@ - '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_hot_recommend_threshold', 'label' => '热帖推荐分阈值', 'type' => 'number', 'default' => 500, 'min' => 1, 'help' => '推荐分达到该值触发热帖奖励与勋章累计'], - ['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 = 不限'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumHomepage.php b/src/AKSRT/Admin/Schema/SchemaForumHomepage.php deleted file mode 100644 index 8621d7a0..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumHomepage.php +++ /dev/null @@ -1,32 +0,0 @@ - '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], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumNames.php b/src/AKSRT/Admin/Schema/SchemaForumNames.php deleted file mode 100644 index d8622ba3..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumNames.php +++ /dev/null @@ -1,36 +0,0 @@ - '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' => '关注'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumOther.php b/src/AKSRT/Admin/Schema/SchemaForumOther.php deleted file mode 100644 index ab10ff8f..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumOther.php +++ /dev/null @@ -1,46 +0,0 @@ - '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' => true], - ['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'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumPermissions.php b/src/AKSRT/Admin/Schema/SchemaForumPermissions.php deleted file mode 100644 index 861de46d..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumPermissions.php +++ /dev/null @@ -1,46 +0,0 @@ - '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' => true], - ['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' => true], - ['key' => 'bbs_post_pay_vip_price_s', 'label' => 'VIP 特价', 'type' => 'switch', 'default' => true], - ['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'], - ]], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumPostPage.php b/src/AKSRT/Admin/Schema/SchemaForumPostPage.php deleted file mode 100644 index fed7afbe..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumPostPage.php +++ /dev/null @@ -1,46 +0,0 @@ - '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], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumReply.php b/src/AKSRT/Admin/Schema/SchemaForumReply.php deleted file mode 100644 index 4094a29a..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumReply.php +++ /dev/null @@ -1,39 +0,0 @@ - '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' => true], - ['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' => true], - ['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' => '写下你的回复…'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaForumUpload.php b/src/AKSRT/Admin/Schema/SchemaForumUpload.php deleted file mode 100644 index 02938bec..00000000 --- a/src/AKSRT/Admin/Schema/SchemaForumUpload.php +++ /dev/null @@ -1,29 +0,0 @@ - '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' => '逗号分隔'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaPageDisplay.php b/src/AKSRT/Admin/Schema/SchemaPageDisplay.php index 390a0b6c..a3a8442f 100644 --- a/src/AKSRT/Admin/Schema/SchemaPageDisplay.php +++ b/src/AKSRT/Admin/Schema/SchemaPageDisplay.php @@ -98,9 +98,6 @@ class SchemaPageDisplay { ['label' => '用户页', 'value' => 'author'], ['label' => '文章页', 'value' => 'single'], ['label' => '搜索页', 'value' => 'search'], - ['label' => '[论坛]首页', 'value' => 'forum_home'], - ['label' => '[论坛]板块页面', 'value' => 'forum_plate'], - ['label' => '[论坛]帖子页面', 'value' => 'forum_post'], ['label' => '其它页面', 'value' => 'page'], ]], ['key' => 'header_components_device_visibility', 'label' => '设备显示规则', 'type' => 'radio', 'default' => '', 'options' => [ @@ -255,7 +252,7 @@ class SchemaPageDisplay { ['key' => 'user_home_paging_ajax', 'label' => 'AJAX 翻页', 'type' => 'switch', 'default' => true], ]], ['key' => 'tabs', 'title' => 'Tab 配置', 'fields' => [ - ['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum'], + ['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits'], ['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,favorites,comments,follow'], ['key' => 'user_home_tabs_pc_hide', 'label' => 'PC 端隐藏', 'type' => 'input', 'default' => ''], ['key' => 'user_home_tabs_mobile_hide', 'label' => '移动端隐藏', 'type' => 'input', 'default' => ''], diff --git a/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php index 2705a9da..26732467 100644 --- a/src/AKSRT/Admin/Schema/SchemaUsers.php +++ b/src/AKSRT/Admin/Schema/SchemaUsers.php @@ -23,10 +23,7 @@ class SchemaUsers { return [ 'day_max' => 100, 'sign_up' => 20, 'sign_in' => 5, 'followed' => 5, 'post_new' => 5, 'post_like' => 1, 'post_favorite' => 2, - 'comment_new' => 2, 'comment_like' => 1, 'bbs_posts_new' => 3, - 'bbs_score_extra' => 1, 'bbs_essence' => 2, 'bbs_posts_hot' => 2, - 'bbs_plate_new' => 2, 'bbs_plate_hot' => 2, 'bbs_adopt' => 2, - 'bbs_comment_hot' => 2, + 'comment_new' => 2, 'comment_like' => 1, ]; } @@ -459,13 +456,6 @@ class SchemaUsers { ['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], ['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], ['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100], - ['key' => 'points_free_bbs_posts_new', 'label' => '论坛发帖', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_reply', 'label' => '论坛回复', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_essence', 'label' => '帖子加精', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_adopt', 'label' => '回复被采纳', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_posts_hot', 'label' => '热帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_comment_hot', 'label' => '神评奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000], - ['key' => 'points_free_bbs_score_extra', 'label' => '帖子加分', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 1000], ['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'], ]], ['key' => 'spend', 'title' => '积分消费', 'fields' => [ diff --git a/src/AKSRT/Admin/SchemaProvider.php b/src/AKSRT/Admin/SchemaProvider.php index cd5a2a4a..c8ff8c06 100644 --- a/src/AKSRT/Admin/SchemaProvider.php +++ b/src/AKSRT/Admin/SchemaProvider.php @@ -43,8 +43,6 @@ class SchemaProvider { 'payment', 'extensions', 'tools', - 'shop', - 'forum', ]; } @@ -92,8 +90,6 @@ class SchemaProvider { 'payment' => 'aksrt_settings', 'extensions' => 'aksrt_settings', 'tools' => 'aksrt_settings', - 'shop' => 'aksrt_settings', - 'forum' => 'aksrt_settings', ]; return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings'; @@ -275,8 +271,6 @@ class SchemaProvider { 'payment' => Schema\SchemaPayment::class, 'extensions' => Schema\SchemaExtensions::class, 'tools' => Schema\SchemaTools::class, - 'shop' => Schema\SchemaShop::class, - 'forum' => Schema\SchemaForum::class, ]; } diff --git a/src/AKSRT/Admin/ToolsController.php b/src/AKSRT/Admin/ToolsController.php index 34922511..829e8e72 100644 --- a/src/AKSRT/Admin/ToolsController.php +++ b/src/AKSRT/Admin/ToolsController.php @@ -155,13 +155,6 @@ class ToolsController extends ControllerBase { } return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]); - case 'rebuild_index': - // Rebuild search index / recount stats - if (class_exists('\AKSRT\Forum\Forum') && method_exists(\AKSRT\Forum\Forum::get_instance(), 'recount_stats')) { - \AKSRT\Forum\Forum::get_instance()->recount_stats(); - } - return $this->success(['message' => __('索引重建完成', 'aksrt')]); - default: return $this->error('未知的维护操作: ' . esc_html($action)); } diff --git a/src/AKSRT/Content/PayRest.php b/src/AKSRT/Content/PayRest.php index 8461383f..8ff5e75f 100644 --- a/src/AKSRT/Content/PayRest.php +++ b/src/AKSRT/Content/PayRest.php @@ -152,11 +152,10 @@ trait PayRest { $order = $repo->get_user_order($uid, $order_number); } else { $order = $repo->get($order_number); - // Guest may only poll own guest cash orders (user_id=0): content/topic resource or plate_follow. + // Guest may only poll own guest cash orders (user_id=0): content resource. $ptype = sanitize_key((string) ($order['product_type'] ?? '')); $guest_types = [ \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, ]; if ($order && ((int) ($order['user_id'] ?? -1) !== 0 || !in_array($ptype, $guest_types, true))) { $order = null; diff --git a/src/AKSRT/Core/DBMigrator.php b/src/AKSRT/Core/DBMigrator.php index cda7aff3..b0e4642e 100644 --- a/src/AKSRT/Core/DBMigrator.php +++ b/src/AKSRT/Core/DBMigrator.php @@ -17,12 +17,6 @@ class DBMigrator { const OPT_KEY = 'aksrt_db_schema_version'; - /** - * Historical flag still readable by older code paths. - * Plate CPT is now native when registered; prefer post_type_exists(). - */ - const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated'; - /** * Register hooks. */ diff --git a/src/AKSRT/Core/DomainModel.php b/src/AKSRT/Core/DomainModel.php index 0f65b62b..8640f164 100644 --- a/src/AKSRT/Core/DomainModel.php +++ b/src/AKSRT/Core/DomainModel.php @@ -31,8 +31,6 @@ class DomainModel { /** Cash purchase of points (zibll order_type=9). */ public const ORDER_PRODUCT_POINTS_BUY = 'points_buy'; public const ORDER_PRODUCT_SHOP = 'shop_product'; - /** Paid plate follow (zibll order_type=11). product_id = plate post ID. */ - public const ORDER_PRODUCT_PLATE_FOLLOW = 'plate_follow'; /** * Human labels for aksrt_order.product_type (front-end order center). @@ -46,7 +44,6 @@ class DomainModel { self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'), self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'), self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'), - self::ORDER_PRODUCT_PLATE_FOLLOW => __('关注版块', 'aksrt'), ]; } diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index 9f59d5ff..fbe88914 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -735,24 +735,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // Forum pages (home / plate / board tax / tag / topic / compose / plate-cat) - $is_forum_surface = is_page_template('templates/pages/page-forums.php') - || is_page_template('templates/forum/forum-compose.php') - || is_tax('aksrt_forum_board') - || is_tax('aksrt_forum_tag') - || is_tax('aksrt_plate_cat') - || is_singular('aksrt_forum_topic') - || is_singular('aksrt_forum_plate'); - - if ($is_forum_surface) { - wp_enqueue_style( - 'aksrt-forum', - $base_uri . '/assets/css/frontend/forum.css', - ['aksrt-tokens'], - $version - ); - } - // Product archive pages (category / tag) if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) { wp_enqueue_style( @@ -826,51 +808,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; ); } - // Forum edit page - if (is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1') { - $forum_edit_js = get_template_directory() . '/assets/js/forum-edit.js'; - if (file_exists($forum_edit_js)) { - wp_enqueue_script( - 'aksrt-forum-edit', - $base_uri . '/assets/js/forum-edit.js', - ['aksrt-script'], - (string) filemtime($forum_edit_js), - true - ); - } - } - - if ($is_forum_surface) { - $forum_governance_js = get_template_directory() . '/assets/js/forum-governance.js'; - if (file_exists($forum_governance_js)) { - wp_enqueue_script( - 'aksrt-forum-governance', - $base_uri . '/assets/js/forum-governance.js', - ['aksrt-script'], - (string) filemtime($forum_governance_js), - true - ); - wp_localize_script('aksrt-forum-governance', 'aksrtForumGovernance', [ - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'restUrl' => esc_url_raw(rest_url()), - 'restNonce' => wp_create_nonce('wp_rest'), - 'publicNonce' => wp_create_nonce('aksrt_public_nonce'), - 'i18n' => [ - 'empty' => __('内容空空如也', 'aksrt'), - 'loadMore' => __('加载更多', 'aksrt'), - 'loading' => __('加载中…', 'aksrt'), - ], - ]); - } - - // §7.4: enqueue media on compose and edit pages - $is_compose = is_page_template('templates/forum/forum-compose.php'); - $is_edit = is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1'; - if ($is_compose || $is_edit) { - wp_enqueue_media(); - } - } - // 商品编辑页共用 page-edit.js if (is_page_template('templates/pages/page-product-edit.php')) { $page_edit_js = get_template_directory() . '/assets/js/page-edit.js'; @@ -901,7 +838,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } // 单篇文章 — poster-share.js(海报分享按钮) - if (is_singular('post') || is_singular('aksrt_forum_topic')) { + if (is_singular('post')) { $poster_js = get_template_directory() . '/assets/js/poster-share.js'; if (file_exists($poster_js)) { wp_enqueue_script( @@ -977,20 +914,8 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; $enqueue_module('resource-archive'); } - // 付费论坛主题:多网关收银台(与内容共用 cms-download.js) - $is_pay_forum_topic = is_singular('aksrt_forum_topic') - && sanitize_key((string) get_post_meta(get_queried_object_id(), '_aksrt_visibility', true)) === 'pay'; - - // 版块现金付费关注:多网关收银台 - $is_pay_forum_plate = false; - if (is_singular('aksrt_forum_plate')) { - $plate_qid = get_queried_object_id(); - $is_pay_forum_plate = (int) get_post_meta($plate_qid, '_aksrt_follow_pay', true) === 1 - && sanitize_key((string) (get_post_meta($plate_qid, '_aksrt_follow_pay_modo', true) ?: 'money')) === 'money'; - } - - // 单篇资源 / 付费内容 / 独立下载页 / 付费主题 / 付费关注版块:下载 + 收银台脚本 - if ($is_single_resource || $is_paid_content_single || $is_download_file_page || $is_pay_forum_topic || $is_pay_forum_plate) { + // 单篇资源 / 付费内容 / 独立下载页:下载 + 收银台脚本 + if ($is_single_resource || $is_paid_content_single || $is_download_file_page) { $cms_download_js_path = get_template_directory() . '/assets/js/cms-download.js'; wp_enqueue_script( 'aksrt-cms-download', diff --git a/src/AKSRT/Core/ScriptsPart1c.php b/src/AKSRT/Core/ScriptsPart1c.php index 82c3169c..cef91583 100644 --- a/src/AKSRT/Core/ScriptsPart1c.php +++ b/src/AKSRT/Core/ScriptsPart1c.php @@ -436,9 +436,8 @@ trait ScriptsPart1c { } - // Comment toolbar (smilie/code/image/quick shortcodes) — post form or forum topic reply. - $load_comment_tools = (is_singular() && comments_open()) - || (is_singular('aksrt_forum_topic') || is_singular('forum_topic')); + // Comment toolbar (smilie/code/image/quick shortcodes) — post form. + $load_comment_tools = (is_singular() && comments_open()); if ($load_comment_tools) { $comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js'; wp_enqueue_script( @@ -474,7 +473,7 @@ trait ScriptsPart1c { } } - if (is_singular() || is_page_template('templates/pages/page-forums.php')) { + if (is_singular()) { $page_template_path = get_template_directory() . '/assets/js/page-template.js'; if (file_exists($page_template_path)) { wp_enqueue_script( diff --git a/src/AKSRT/Core/ScriptsPart2.php b/src/AKSRT/Core/ScriptsPart2.php index bd6a72c8..d88803e1 100644 --- a/src/AKSRT/Core/ScriptsPart2.php +++ b/src/AKSRT/Core/ScriptsPart2.php @@ -222,36 +222,6 @@ trait ScriptsPart2 { } } - } elseif ($type === 'topic' && post_type_exists('aksrt_topic')) { - $posts = get_posts([ - 'post_type' => 'aksrt_topic', - 'post_status' => 'publish', - 's' => $q, - 'posts_per_page' => 6, - 'no_found_rows' => true, - ]); - foreach ($posts as $p) { - $items[] = [ - 'title' => $p->post_title, - 'url' => get_permalink($p), - 'meta' => get_the_date('', $p), - ]; - } - } elseif ($type === 'product' && post_type_exists('aksrt_product')) { - $posts = get_posts([ - 'post_type' => 'aksrt_product', - 'post_status' => 'publish', - 's' => $q, - 'posts_per_page' => 6, - 'no_found_rows' => true, - ]); - foreach ($posts as $p) { - $items[] = [ - 'title' => $p->post_title, - 'url' => get_permalink($p), - 'meta' => get_the_date('', $p), - ]; - } } elseif ($type === 'tag') { $terms = get_terms([ 'taxonomy' => 'post_tag', diff --git a/src/AKSRT/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index a99dfc97..85d30190 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -382,29 +382,6 @@ trait SetupPart1 { 'after_title' => $title_after, ]); - // ── Forum / BBS widget areas (zibll bbs_* parity lite) ── - $forum_widgets = [ - 'bbs_home_sidebar' => [__('论坛首页 · 侧栏', 'aksrt'), __('论坛首页侧栏区域。', 'aksrt')], - 'bbs_home_top_fluid' => [__('论坛首页 · 顶部全宽', 'aksrt'), __('论坛首页顶部全宽。', 'aksrt')], - 'bbs_plate_sidebar' => [__('论坛版块 · 侧栏', 'aksrt'), __('版块页侧栏。', 'aksrt')], - 'bbs_plate_top_fluid' => [__('论坛版块 · 顶部全宽', 'aksrt'), __('版块页顶部全宽。', 'aksrt')], - 'bbs_posts_sidebar' => [__('论坛帖子 · 侧栏', 'aksrt'), __('帖子页侧栏挂件区。', 'aksrt')], - 'bbs_posts_top_fluid' => [__('论坛帖子 · 顶部全宽', 'aksrt'), __('帖子页顶部全宽。', 'aksrt')], - 'bbs_new_posts_sidebar_top' => [__('论坛发帖 · 侧栏顶部', 'aksrt'), __('发帖页侧栏顶部。', 'aksrt')], - 'bbs_new_posts_sidebar_bottom' => [__('论坛发帖 · 侧栏底部', 'aksrt'), __('发帖页侧栏底部。', 'aksrt')], - ]; - foreach ($forum_widgets as $sid => $meta) { - register_sidebar([ - 'id' => $sid, - 'name' => $meta[0], - 'description' => $meta[1], - 'before_widget' => $card_wrapper_before, - 'after_widget' => '', - 'before_title' => $title_before, - 'after_title' => $title_after, - ]); - } - // ── Shop widget areas (§6.9) ── $shop_widgets = [ 'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'], diff --git a/src/AKSRT/Core/SetupPart2.php b/src/AKSRT/Core/SetupPart2.php index ce2d6191..a92a175f 100644 --- a/src/AKSRT/Core/SetupPart2.php +++ b/src/AKSRT/Core/SetupPart2.php @@ -32,9 +32,6 @@ trait SetupPart2 { if (!isset($menu[34])) { $menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator']; } - if (!isset($menu[39])) { - $menu[39] = ['', 'read', 'separator-aksrt-forum', '', 'wp-menu-separator']; - } } /** diff --git a/src/AKSRT/Core/Shortcodes.php b/src/AKSRT/Core/Shortcodes.php index 65dde2e8..a92bd57b 100644 --- a/src/AKSRT/Core/Shortcodes.php +++ b/src/AKSRT/Core/Shortcodes.php @@ -178,29 +178,6 @@ class Shortcodes { return true; } - // Forum topics: paid/credits unlock list or orders. - if ($post->post_type === 'aksrt_forum_topic' && class_exists('\AKSRT\Forum\Forum')) { - $forum = \AKSRT\Forum\Forum::get_instance(); - $visibility = sanitize_key((string) get_post_meta($pid, '_aksrt_visibility', true)); - if ($visibility === 'public' || $visibility === '') { - return true; - } - if (method_exists($forum, 'has_user_unlocked_topic') && $forum->has_user_unlocked_topic($uid, $pid)) { - return true; - } - if (method_exists($forum, 'has_user_paid_topic') && $forum->has_user_paid_topic($uid, $pid)) { - return true; - } - // Free VIP price counts as paid for cash topics. - if ($visibility === 'pay' && method_exists($forum, 'resolve_topic_pay_price')) { - $price = $forum->resolve_topic_pay_price($pid, $uid); - if (!empty($price['is_free'])) { - return true; - } - } - return false; - } - if (class_exists('\AKSRT\Content\Visibility')) { return \AKSRT\Content\Visibility::can_view_paid_content($pid); } diff --git a/src/AKSRT/Core/Templates.php b/src/AKSRT/Core/Templates.php index afe37e70..510bd6b6 100644 --- a/src/AKSRT/Core/Templates.php +++ b/src/AKSRT/Core/Templates.php @@ -24,7 +24,6 @@ class Templates { public function init(): void { add_filter('theme_page_templates', [$this, 'register_page_templates']); add_filter('template_include', [$this, 'load_page_template']); - add_filter('template_include', [$this, 'load_taxonomy_template']); add_filter('page_template_hierarchy', [$this, 'template_hierarchy']); add_filter('aksrt_page_template_hierarchy', [$this, 'template_hierarchy']); @@ -51,12 +50,7 @@ class Templates { 'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'), 'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'), 'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'), - 'templates/pages/page-cart.php' => __('AKSRT - 购物车', 'aksrt'), 'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'), - 'templates/pages/page-product.php' => __('AKSRT - 商品详情', 'aksrt'), - 'templates/pages/page-product-edit.php' => __('AKSRT - 商品编辑', 'aksrt'), - 'templates/pages/page-forums.php' => __('AKSRT - 论坛', 'aksrt'), - 'templates/forum/forum-compose.php' => __('AKSRT - 发帖', 'aksrt'), ]; } @@ -104,23 +98,9 @@ class Templates { } /** - * Load dedicated product taxonomy templates. + * Load dedicated taxonomy templates. */ public function load_taxonomy_template(string $template): string { - if (is_tax('aksrt_product_cat')) { - $cat_template = AKSRT_DIR . '/templates/shop/product-cat.php'; - if (file_exists($cat_template)) { - return $cat_template; - } - } - - if (is_tax('aksrt_product_tag')) { - $tag_template = AKSRT_DIR . '/templates/shop/product-tag.php'; - if (file_exists($tag_template)) { - return $tag_template; - } - } - return $template; } diff --git a/src/AKSRT/Core/WidgetAjax.php b/src/AKSRT/Core/WidgetAjax.php index 3c013bb7..ce64839f 100644 --- a/src/AKSRT/Core/WidgetAjax.php +++ b/src/AKSRT/Core/WidgetAjax.php @@ -35,9 +35,6 @@ class WidgetAjax { '/widgets/oneline-posts' => 'rest_oneline_posts', '/widgets/recent-comments' => 'rest_recent_comments', '/widgets/user-lists' => 'rest_user_lists', - '/widgets/forum-topics' => 'rest_forum_topics', - '/widgets/forum-terms' => 'rest_forum_terms', - '/widgets/forum-boards' => 'rest_forum_boards', '/widgets/tag-cloud' => 'rest_tag_cloud', '/widgets/categories' => 'rest_categories', '/widgets/term-cards' => 'rest_term_cards', @@ -264,42 +261,6 @@ class WidgetAjax { return new \WP_REST_Response(['success' => true, 'html' => $html], 200); } - public function rest_forum_topics(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) { - return $this->fail(); - } - $style = sanitize_key((string) ($request->get_param('style') ?? 'mini')); - if (!in_array($style, ['mini', 'rank'], true)) { - $style = 'mini'; - } - $instance = [ - 'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))), - 'period' => sanitize_key((string) ($request->get_param('period') ?? 'week')), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'views')), - 'style' => $style, - 'plate' => max(0, (int) ($request->get_param('plate') ?? $request->get_param('plate_id') ?? 0)), - 'paginate' => in_array(sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')) : 'ajax', - ]; - $page = max(1, (int) ($request->get_param('page') ?? 1)); - return $this->payload(\AKSRT\Widgets\WidgetForumHotTopics::query_payload($instance, $page), $page); - } - - public function rest_forum_terms(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumTopicTerms')) { - return $this->fail(); - } - $instance = [ - 'number' => max(1, min(40, (int) ($request->get_param('number') ?? 12))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')), - 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), - 'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')), - ]; - $html = \AKSRT\Widgets\WidgetForumTopicTerms::render_html($instance); - return new \WP_REST_Response(['success' => true, 'html' => $html], 200); - } - - public function rest_category_block(\WP_REST_Request $request) { if (!class_exists('\AKSRT\Widgets\CategoryBlockWidget')) { return $this->fail(); @@ -337,20 +298,6 @@ class WidgetAjax { return new \WP_REST_Response(['success' => true, 'html' => $html], 200); } - public function rest_forum_boards(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumBoardList')) { - return $this->fail(); - } - $instance = [ - 'count' => max(1, min(50, (int) ($request->get_param('count') ?? 30))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'list')), - 'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'), - ]; - $html = \AKSRT\Widgets\WidgetForumBoardList::render_html($instance); - return new \WP_REST_Response(['success' => true, 'html' => $html], 200); - } - public function rest_tag_cloud(\WP_REST_Request $request) { if (!class_exists('\AKSRT\Widgets\TagCloudWidget')) { return $this->fail(); diff --git a/src/AKSRT/Credits/Earn.php b/src/AKSRT/Credits/Earn.php index 61115c02..257e1486 100644 --- a/src/AKSRT/Credits/Earn.php +++ b/src/AKSRT/Credits/Earn.php @@ -32,10 +32,6 @@ class Earn { add_action('aksrt_post_favorited', [$this, 'on_post_favorited'], 10, 3); add_action('aksrt_comment_liked', [$this, 'on_comment_liked'], 10, 3); add_action('aksrt_user_followed', [$this, 'on_user_followed'], 10, 2); - - add_action('aksrt_forum_topic_created', [$this, 'on_forum_topic_created'], 10, 2); - add_action('comment_post', [$this, 'on_forum_reply_posted'], 20, 3); - add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 10, 2); } public function on_user_register(int $user_id): void { @@ -124,11 +120,6 @@ class Earn { if (get_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', true)) { return; } - // Skip forum topic replies (handled separately). - $post = get_post((int) $comment->comment_post_ID); - if ($post && $post->post_type === 'aksrt_forum_topic') { - return; - } $cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_credit_settings() : []; $reward = is_array($cfg['comment_reward'] ?? null) ? $cfg['comment_reward'] : []; $min_length = (int) ($reward['min_length'] ?? 0); @@ -200,48 +191,4 @@ class Earn { } PointsFree::grant($following_id, 'followed', 0, ['type' => 'user', 'id' => $follower_id]); } - - public function on_forum_topic_created(int $user_id, int $post_id): void { - if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) { - return; - } - PointsFree::grant($user_id, 'bbs_posts_new', 0, ['type' => 'post', 'id' => $post_id]); - } - - public function on_forum_reply_posted(int $comment_id, $approved, array $commentdata = []): void { - unset($commentdata); - if ((int) $approved !== 1 && $approved !== '1' && $approved !== true) { - // Still load comment and check. - } - $comment = get_comment($comment_id); - if (!$comment || (int) $comment->comment_approved !== 1) { - return; - } - $post = get_post((int) $comment->comment_post_ID); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $user_id = (int) $comment->user_id; - if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) { - return; - } - if (get_comment_meta($comment_id, 'aksrt_credit_forum_reply', true)) { - return; - } - $granted = PointsFree::grant($user_id, 'bbs_reply', 0, ['type' => 'comment', 'id' => $comment_id]); - if (!is_wp_error($granted)) { - update_comment_meta($comment_id, 'aksrt_credit_forum_reply', 1); - } - } - - /** - * Essence reward is owned by \AKSRT\Forum\Forum::reward_essence() (free-day-cap). - * Keep hook for external integrations only — do not double-grant. - * - * @param int $topic_id - * @param int $author_or_operator Author id from forum (2nd arg) or operator. - */ - public function on_forum_topic_featured(int $topic_id, int $author_or_operator = 0): void { - unset($topic_id, $author_or_operator); - } } diff --git a/src/AKSRT/Credits/PointsFree.php b/src/AKSRT/Credits/PointsFree.php index a36df371..c38ab0ee 100644 --- a/src/AKSRT/Credits/PointsFree.php +++ b/src/AKSRT/Credits/PointsFree.php @@ -30,13 +30,6 @@ class PointsFree { 'post_favorite' => [__('文章被收藏', 'aksrt'), __('你的文章被收藏', 'aksrt'), 1], 'comment_new' => [__('发表评论', 'aksrt'), __('发表并通过审核的评论', 'aksrt'), 1], 'comment_like' => [__('评论被赞', 'aksrt'), __('你的评论被点赞', 'aksrt'), 1], - 'bbs_posts_new' => [__('论坛发帖', 'aksrt'), __('发布论坛主题', 'aksrt'), 5], - 'bbs_reply' => [__('论坛回复', 'aksrt'), __('回复论坛主题', 'aksrt'), 2], - 'bbs_essence' => [__('帖子加精', 'aksrt'), __('主题被设为精华', 'aksrt'), 10], - 'bbs_adopt' => [__('回复被采纳', 'aksrt'), __('问答帖回复被采纳', 'aksrt'), 20], - 'bbs_posts_hot' => [__('热帖奖励', 'aksrt'), __('主题成为热帖', 'aksrt'), 5], - 'bbs_comment_hot'=> [__('神评奖励', 'aksrt'), __('回复成为神评', 'aksrt'), 5], - 'bbs_score_extra'=> [__('帖子加分', 'aksrt'), __('主题被加分时奖励', 'aksrt'), 3], ]; } @@ -194,8 +187,8 @@ class PointsFree { $task_dates = is_array($raw) ? $raw : []; } - $passive = ['post_like', 'post_favorite', 'followed', 'comment_like', 'bbs_essence', 'bbs_adopt', 'bbs_posts_hot', 'bbs_comment_hot', 'bbs_score_extra']; - $active_once = ['post_new', 'comment_new', 'bbs_posts_new', 'bbs_reply']; + $passive = ['post_like', 'post_favorite', 'followed', 'comment_like']; + $active_once = ['post_new', 'comment_new']; foreach (self::catalog() as $key => $meta) { $pts = (int) ($cfg['tasks'][$key] ?? 0); @@ -333,48 +326,6 @@ class PointsFree { return 0; } return max(0, (int) ($cr['amount'] ?? $default)); - case 'bbs_posts_new': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['post_new'] ?? $f['credits']['topic_new'] ?? $all['bbs_credits_post_new'] ?? $default)); - } - return max(0, (int) ($all['bbs_credits_post_new'] ?? $default)); - case 'bbs_reply': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['comment_new'] ?? $default)); - } - return $default; - case 'bbs_essence': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['essence'] ?? $default)); - } - return $default; - case 'bbs_adopt': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['adopted'] ?? $default)); - } - return $default; - case 'bbs_posts_hot': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['posts_hot'] ?? $default)); - } - return $default; - case 'bbs_comment_hot': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['comment_hot'] ?? $f['credits']['comment_like'] ?? $default)); - } - return $default; - case 'bbs_score_extra': - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $f = \AKSRT\Settings\Settings::get_forum_settings(); - return max(0, (int) ($f['credits']['score_extra'] ?? $default)); - } - return $default; default: return $default; } diff --git a/src/AKSRT/Forum/BoardService.php b/src/AKSRT/Forum/BoardService.php deleted file mode 100644 index e6b7c9b0..00000000 --- a/src/AKSRT/Forum/BoardService.php +++ /dev/null @@ -1,396 +0,0 @@ -forum = $forum; - } - - /** - * Get board list (plate CPT). - * - * 读取 aksrt_forum_plate CPT,支持层级查询。 - * Fallback:若 plate CPT 不存在或迁移未完成,降级读取 taxonomy。 - */ - public function get_boards(array $args = []): array { - // 优先使用 plate CPT - if ($this->forum->is_plate_cpt_active()) { - return $this->get_boards_from_cpt($args); - } - - // Fallback: 读取旧 taxonomy(向后兼容) - return $this->get_boards_from_taxonomy(); - } - - /** - * 从 plate CPT 读取板块列表(新实现)。 - */ - private function get_boards_from_cpt(array $args = []): array { - $defaults = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'hierarchical' => true, - ]; - - $query_args = wp_parse_args($args, $defaults); - $plates = get_posts($query_args); - - if (empty($plates)) { - return []; - } - - $boards = []; - foreach ($plates as $plate) { - $plate_id = (int) $plate->ID; - $moderators = $this->forum->get_plate_moderators($plate_id); - $applications = $this->forum->get_plate_moderator_applications($plate_id); - - $boards[] = [ - 'id' => $plate_id, - 'name' => (string) $plate->post_title, - 'description' => (string) $plate->post_excerpt, - 'content' => (string) $plate->post_content, - 'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true), - 'sort_order' => (int) $plate->menu_order, - 'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true), - 'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true), - 'views' => (int) get_post_meta($plate_id, '_aksrt_views', true), - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true), - 'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true), - 'moderator_count' => count($moderators), - 'pending_application_count' => count(array_filter($applications, static function ($item): bool { - return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING; - })), - 'url' => (string) get_permalink($plate), - 'author_id' => (int) $plate->post_author, - 'parent_id' => (int) $plate->post_parent, - 'created_at' => (string) $plate->post_date, - 'thumbnail_url' => (string) get_the_post_thumbnail_url($plate, 'thumbnail'), - ]; - } - - return $this->build_hierarchical_boards($boards); - } - - /** - * 从 taxonomy 读取板块(向后兼容)。 - */ - private function get_boards_from_taxonomy(): array { - $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)) { - return []; - } - - $boards = []; - foreach ($terms as $term) { - $board_id = (int) $term->term_id; - $moderators = $this->forum->get_board_moderators($board_id); - $applications = $this->forum->get_board_moderator_applications($board_id); - $board_link = get_term_link($term); - - $boards[] = [ - 'id' => $board_id, - 'name' => (string) $term->name, - 'description' => (string) $term->description, - 'content' => '', - 'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true), - 'sort_order' => (int) get_term_meta($board_id, '_aksrt_sort_order', true), - 'post_count' => (int) $term->count, - 'reply_count' => 0, - 'today_reply_count' => 0, - 'views' => 0, - 'follow_count' => 0, - 'last_reply' => '', - 'last_post' => '', - 'moderator_count' => count($moderators), - 'pending_application_count' => count(array_filter($applications, static function ($item): bool { - return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING; - })), - 'url' => !is_wp_error($board_link) ? (string) $board_link : '', - 'author_id' => 0, - 'parent_id' => (int) $term->parent, - 'created_at' => '', - 'thumbnail_url' => '', - ]; - } - - return $boards; - } - - /** - * 构建层级板块树(子板块嵌套)。 - */ - private function build_hierarchical_boards(array $boards): array { - $by_id = []; - foreach ($boards as $board) { - $by_id[$board['id']] = $board; - $by_id[$board['id']]['children'] = []; - } - - $roots = []; - foreach ($boards as $board) { - if ($board['parent_id'] > 0 && isset($by_id[$board['parent_id']])) { - $by_id[$board['parent_id']]['children'][] = &$by_id[$board['id']]; - } else { - $roots[] = &$by_id[$board['id']]; - } - } - - return $roots; - } - - /** - * Create board (统一入口,自动选择 CPT/taxonomy). - */ - public function create_board(array $data): int { - if ($this->forum->is_plate_cpt_active()) { - return $this->create_plate($data); - } - - // Fallback: taxonomy - $name = sanitize_text_field((string) ($data['name'] ?? '')); - if ($name === '') { - return 0; - } - - $result = wp_insert_term($name, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($data['description'] ?? '')), - ]); - - if (is_wp_error($result)) { - return 0; - } - - $term_id = (int) $result['term_id']; - $icon = sanitize_text_field((string) ($data['icon'] ?? '')); - $sort_order = (int) ($data['sort_order'] ?? $term_id); - - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - - update_term_meta($term_id, '_aksrt_sort_order', $sort_order); - - return $term_id; - } - - /** - * Update board (统一入口,自动选择 CPT/taxonomy). - */ - public function update_board(int $board_id, array $data): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->update_plate($board_id, $data); - } - - // Fallback: taxonomy - $term = get_term($board_id, 'aksrt_forum_board'); - if (!$term || is_wp_error($term)) { - return false; - } - - $update = ['taxonomy' => 'aksrt_forum_board']; - - if (isset($data['name'])) { - $name = sanitize_text_field((string) $data['name']); - if ($name !== '') { - $update['name'] = $name; - } - } - - if (isset($data['description'])) { - $update['description'] = sanitize_textarea_field((string) $data['description']); - } - - $result = wp_update_term($board_id, 'aksrt_forum_board', $update); - if (is_wp_error($result)) { - return false; - } - - if (isset($data['icon'])) { - update_term_meta($board_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - - if (isset($data['sort_order'])) { - update_term_meta($board_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - - return true; - } - - /** - * Delete board (统一入口,自动选择 CPT/taxonomy). - */ - public function delete_board(int $board_id): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->delete_plate($board_id); - } - - // Fallback: taxonomy - $term = get_term($board_id, 'aksrt_forum_board'); - if (!$term || is_wp_error($term)) { - return false; - } - - $result = wp_delete_term($board_id, 'aksrt_forum_board'); - return !is_wp_error($result) && !empty($result); - } - - /** - * Reorder boards (统一入口). - */ - public function reorder_boards(array $order_map): bool { - if ($this->forum->is_plate_cpt_active()) { - return $this->reorder_plates($order_map); - } - - foreach ($order_map as $term_id => $sort_order) { - $term_id = (int) $term_id; - if ($term_id <= 0) { - continue; - } - - update_term_meta($term_id, '_aksrt_sort_order', (int) $sort_order); - } - - return true; - } - - // ================================================================ - // §7.0 Plate CPT methods —板块 CPT 化 CRUD - // ================================================================ - - /** - * Get all plates (板块 CPT posts). - * - * @return array> - */ - public function get_plates(array $args = []): array { - $query_args = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => (int) ($args['limit'] ?? 100), - 'orderby' => 'menu_order', - 'order' => 'ASC', - ]; - - if (!empty($args['parent'])) { - $query_args['post_parent'] = (int) $args['parent']; - } - - if (!empty($args['plate_cat'])) { - $query_args['tax_query'] = [[ - 'taxonomy' => 'aksrt_plate_cat', - 'field' => 'term_id', - 'terms' => (int) $args['plate_cat'], - ]]; - } - - $posts = get_posts($query_args); - $plates = []; - - foreach ($posts as $post) { - $plates[] = $this->build_plate_payload($post); - } - - return $plates; - } - - /** - * Get single plate by ID. - */ - public function get_plate(int $plate_id): ?array { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return null; - } - - return $this->build_plate_payload($post); - } - - /** - * Build plate data payload from \WP_Post. - */ - public function build_plate_payload(\WP_Post $post): array { - $plate_id = (int) $post->ID; - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - $moderators = []; - } - - $cover_url = (string) get_the_post_thumbnail_url($plate_id, 'medium'); - if ($cover_url === '') { - $config = $this->forum->get_config(); - $default_thumbs = $config['other']['bbs_term_thumb'] ?? []; - if (!empty($default_thumbs) && is_array($default_thumbs)) { - $cover_url = $default_thumbs[array_rand($default_thumbs)]; - } - } - - $plate_cats = wp_get_object_terms($plate_id, 'aksrt_plate_cat', ['fields' => 'all']); - $plate_cat = (!is_wp_error($plate_cats) && !empty($plate_cats)) ? [ - 'id' => (int) $plate_cats[0]->term_id, - 'name' => (string) $plate_cats[0]->name, - 'slug' => (string) $plate_cats[0]->slug, - ] : null; - - return [ - 'id' => $plate_id, - 'name' => (string) $post->post_title, - 'description' => (string) $post->post_excerpt, - 'content' => (string) $post->post_content, - 'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true), - 'cover_url' => $cover_url, - 'sort_order' => (int) $post->menu_order, - 'parent_id' => (int) $post->post_parent, - 'author_id' => (int) $post->post_author, - 'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true), - 'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true), - 'views' => (int) get_post_meta($plate_id, '_aksrt_views', true), - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true), - 'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true), - 'moderator_count' => count($moderators), - 'moderators' => $moderators, - 'url' => (string) get_permalink($plate_id), - 'created_at' => (string) $post->post_date, - 'allow_view' => (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'), - 'add_limit' => (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'), - 'is_hot' => $this->forum->is_hot_plate($plate_id), - 'plate_cat' => $plate_cat, - // Alias fields for backward compat with get_board_view consumer - 'topic_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true), - 'thumbnail_url' => $cover_url, - ]; - } - - /** - * Create plate (CPT post). - */ -} diff --git a/src/AKSRT/Forum/BoardServicePart1.php b/src/AKSRT/Forum/BoardServicePart1.php deleted file mode 100644 index a482bd8a..00000000 --- a/src/AKSRT/Forum/BoardServicePart1.php +++ /dev/null @@ -1,425 +0,0 @@ - 'aksrt_forum_plate', - 'post_title' => $title, - 'post_content' => wp_kses_post((string) ($data['content'] ?? '')), - 'post_excerpt' => sanitize_textarea_field((string) ($data['description'] ?? '')), - 'post_status' => 'publish', - 'post_parent' => (int) ($data['parent_id'] ?? 0), - 'post_author' => (int) ($data['author_id'] ?? get_current_user_id()), - 'menu_order' => (int) ($data['sort_order'] ?? 0), - ], true); - - if (is_wp_error($plate_id)) { - return 0; - } - - $icon = sanitize_text_field((string) ($data['icon'] ?? '')); - if ($icon !== '') { - update_post_meta($plate_id, '_aksrt_icon', $icon); - } - - update_post_meta($plate_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0)); - update_post_meta($plate_id, '_aksrt_posts_count', 0); - update_post_meta($plate_id, '_aksrt_reply_count', 0); - update_post_meta($plate_id, '_aksrt_follow_count', 0); - update_post_meta($plate_id, '_aksrt_plate_views', 0); - - if (!empty($data['plate_cat'])) { - wp_set_object_terms($plate_id, [(int) $data['plate_cat']], 'aksrt_plate_cat'); - } - - // Also create a corresponding taxonomy term for backward compat - $term_result = wp_insert_term($title, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($data['description'] ?? '')), - ]); - if (!is_wp_error($term_result)) { - $term_id = (int) $term_result['term_id']; - update_term_meta($term_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0)); - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - update_post_meta($plate_id, '_aksrt_migrated_from_term', $term_id); - } - - return $plate_id; - } - - /** - * Update plate. - */ - public function update_plate(int $plate_id, array $data): bool { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return false; - } - - $update = ['ID' => $plate_id]; - - if (isset($data['name'])) { - $name = sanitize_text_field((string) $data['name']); - if ($name !== '') { - $update['post_title'] = $name; - } - } - - if (isset($data['description'])) { - $update['post_excerpt'] = sanitize_textarea_field((string) $data['description']); - } - - if (isset($data['content'])) { - $update['post_content'] = wp_kses_post((string) $data['content']); - } - - if (isset($data['parent_id'])) { - $update['post_parent'] = (int) $data['parent_id']; - } - - if (isset($data['sort_order'])) { - $update['menu_order'] = (int) $data['sort_order']; - update_post_meta($plate_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - - if (isset($data['author_id'])) { - $update['post_author'] = (int) $data['author_id']; - } - - $result = wp_update_post($update, true); - if (is_wp_error($result)) { - return false; - } - - if (isset($data['icon'])) { - update_post_meta($plate_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - - if (isset($data['allow_view'])) { - $allowed_views = ['public', 'login', 'role', 'follow']; - $view = in_array($data['allow_view'], $allowed_views, true) ? $data['allow_view'] : 'public'; - update_post_meta($plate_id, '_aksrt_allow_view', $view); - } - - // Sync to legacy taxonomy term if exists - $term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true); - if ($term_id > 0) { - $term_update = []; - if (isset($update['post_title'])) { - $term_update['name'] = $update['post_title']; - } - if (isset($update['post_excerpt'])) { - $term_update['description'] = $update['post_excerpt']; - } - if (!empty($term_update)) { - wp_update_term($term_id, 'aksrt_forum_board', $term_update); - } - if (isset($data['icon'])) { - update_term_meta($term_id, '_aksrt_icon', sanitize_text_field((string) $data['icon'])); - } - if (isset($data['sort_order'])) { - update_term_meta($term_id, '_aksrt_sort_order', (int) $data['sort_order']); - } - } - - return true; - } - - /** - * Delete plate (with cascade option). - */ - public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool { - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return false; - } - - $topic_ids = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'meta_key' => '_aksrt_plate_id', - 'meta_value' => $plate_id, - 'posts_per_page' => -1, - 'fields' => 'ids', - 'post_status' => 'any', - ]); - - if (!empty($topic_ids)) { - if ($cascade === 'move' && $move_to > 0 && $move_to !== $plate_id) { - $target = get_post($move_to); - if ($target && $target->post_type === 'aksrt_forum_plate') { - foreach ($topic_ids as $tid) { - update_post_meta((int) $tid, '_aksrt_plate_id', $move_to); - } - $this->forum->sync_plate_counts($move_to); - } - } else { - foreach ($topic_ids as $tid) { - wp_trash_post((int) $tid); - } - } - } - - // Handle child plates (move to top-level) - $children = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_parent' => $plate_id, - 'posts_per_page' => -1, - 'fields' => 'ids', - 'post_status' => 'any', - ]); - foreach ($children as $child_id) { - wp_update_post(['ID' => (int) $child_id, 'post_parent' => 0]); - } - - // Remove legacy taxonomy term if linked - $term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true); - if ($term_id > 0) { - wp_delete_term($term_id, 'aksrt_forum_board'); - } - - wp_delete_post($plate_id, true); - return true; - } - - /** - * Reorder plates. - */ - public function reorder_plates(array $order_map): bool { - foreach ($order_map as $plate_id => $sort_order) { - $plate_id = (int) $plate_id; - if ($plate_id <= 0) continue; - - wp_update_post(['ID' => $plate_id, 'menu_order' => (int) $sort_order]); - update_post_meta($plate_id, '_aksrt_sort_order', (int) $sort_order); - } - - return true; - } - - /** - * Resolve plate_id for a topic (reads _aksrt_plate_id meta, falls back to taxonomy). - */ - public function get_topic_plate_id(int $topic_id): int { - $plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - return $plate_id; - } - - // Fallback: find plate via legacy taxonomy term - $terms = get_the_terms($topic_id, 'aksrt_forum_board'); - if (!$terms || is_wp_error($terms)) { - return 0; - } - - $term = $terms[0]; - // Look up plate post by migrated_from_term meta - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'meta_key' => '_aksrt_migrated_from_term', - 'meta_value' => (string) $term->term_id, - 'numberposts' => 1, - 'fields' => 'ids', - 'post_status' => 'publish', - ]); - - if (!empty($plates)) { - $resolved = (int) $plates[0]; - update_post_meta($topic_id, '_aksrt_plate_id', $resolved); - return $resolved; - } - - return 0; - } - - /** - * Check if a user is a plate moderator (CPT-based). - */ - public function is_plate_moderator(int $user_id, int $plate_id): bool { - if ($user_id <= 0 || $plate_id <= 0) { - return false; - } - - if ($this->forum->is_forum_admin($user_id)) { - return true; - } - - $post = get_post($plate_id); - if ($post && (int) $post->post_author === $user_id) { - return true; - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($moderators) && in_array($user_id, $moderators, true)) { - return true; - } - - return false; - } - - /** - * Get plate view (topics + moderators + pagination) for frontend. - */ - public function get_plate_view(int $plate_id, array $args = []): array { - $plate = $this->get_plate($plate_id); - if (!$plate) { - return [ - 'plate' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'children' => [], - ]; - } - - $user_id = (int) ($args['user_id'] ?? get_current_user_id()); - $paged = max(1, (int) ($args['paged'] ?? 1)); - - $topics = $this->forum->get_topic_list([ - 'plate_id' => $plate_id, - 'page' => $paged, - 'limit' => 20, - ]); - - // Global + super + plate sticky - $sticky_topics = $this->get_sticky_topics_for_plate($plate_id); - - $children = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_parent' => $plate_id, - 'post_status' => 'publish', - 'posts_per_page' => 50, - 'orderby' => 'menu_order', - 'order' => 'ASC', - ]); - - $child_plates = []; - foreach ($children as $child) { - $child_plates[] = $this->build_plate_payload($child); - } - - return [ - 'plate' => $plate, - 'topics' => $topics['items'], - 'sticky_topics' => $sticky_topics, - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id), - 'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'), - 'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id), - 'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id), - 'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id), - 'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id), - ], - 'children' => $child_plates, - ]; - } - - /** - * Get sticky topics for a plate (global + super + plate-level). - */ - private function get_sticky_topics_for_plate(int $plate_id): array { - $global_sticky = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'meta_key' => '_aksrt_sticky', - 'meta_value' => Forum::STICKY_GLOBAL, - 'posts_per_page' => 5, - 'orderby' => 'date', - 'order' => 'DESC', - ]); - - $plate_sticky = get_posts([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'posts_per_page' => 10, - 'orderby' => 'date', - 'order' => 'DESC', - 'meta_query' => [ - 'relation' => 'AND', - ['key' => '_aksrt_plate_id', 'value' => $plate_id, 'type' => 'NUMERIC'], - ['key' => '_aksrt_sticky', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'], - ['key' => '_aksrt_sticky', 'value' => Forum::STICKY_GLOBAL, 'compare' => '<', 'type' => 'NUMERIC'], - ], - ]); - - $items = []; - $seen = []; - - foreach (array_merge($global_sticky, $plate_sticky) as $post) { - if (isset($seen[$post->ID])) continue; - $seen[$post->ID] = true; - $items[] = $this->forum->build_topic_payload($post); - } - - return array_slice($items, 0, 10); - } - - /** - * Get moderator user profiles for a plate. - */ - private function get_plate_moderator_profiles(int $plate_id): array { - $post = get_post($plate_id); - $profiles = []; - - // Author = super moderator - if ($post && $post->post_author > 0) { - $user = get_userdata((int) $post->post_author); - if ($user) { - $profiles[] = [ - 'id' => (int) $user->ID, - 'name' => $user->display_name, - 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user->ID, 40) : get_avatar_url($user->ID, ['size' => 40]), - 'role' => 'plate_author', - ]; - } - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return $profiles; - } - - $roles = get_post_meta($plate_id, '_aksrt_moderator_roles', true); - if (!is_array($roles)) { - $roles = []; - } - - foreach ($moderators as $uid) { - $uid = (int) $uid; - if ($post && $uid === (int) $post->post_author) continue; - - $user = get_userdata($uid); - if (!$user) continue; - - $profiles[] = [ - 'id' => $uid, - 'name' => $user->display_name, - 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($uid, 40) : get_avatar_url($uid, ['size' => 40]), - 'role' => (string) ($roles[$uid] ?? 'moderator'), - ]; - } - - return $profiles; - } -} diff --git a/src/AKSRT/Forum/BoardServicePart2.php b/src/AKSRT/Forum/BoardServicePart2.php deleted file mode 100644 index 87bbce8d..00000000 --- a/src/AKSRT/Forum/BoardServicePart2.php +++ /dev/null @@ -1,796 +0,0 @@ - '全部', 'sort' => 'date'], - ['label' => '推荐', 'sort' => 'featured'], - ['label' => '最新回复', 'sort' => 'last_reply'], - ['label' => '热门', 'sort' => 'hot'], - ['label' => '精华', 'sort' => 'essence'], - ]; - $allowed_sorts = []; - foreach ($tabs as $tab) { - $sort = sanitize_key((string) ($tab['sort'] ?? '')); - if ($sort !== '') { - $allowed_sorts[] = $sort; - } - } - if ($orderby === '' || !in_array($orderby, $allowed_sorts, true)) { - $active_index = max(0, (int) ($board_page['tab_active_index'] ?? 0)); - $orderby = sanitize_key((string) ($tabs[$active_index]['sort'] ?? 'date')); - if ($orderby === '') { - $orderby = 'date'; - } - } - - $compose_url = function_exists('aksrt_get_forum_compose_url') - ? aksrt_get_forum_compose_url(['board' => $board_id]) - : home_url('/'); - $top_info = !isset($board_page['top_info']) || !empty($board_page['top_info']); - - if ($this->forum->is_plate_cpt_active()) { - $view = $this->get_plate_view_compat($board_id, $user_id, $page, $orderby); - $view['tabs'] = $tabs; - $view['active_sort'] = $orderby; - $view['top_info'] = $top_info; - $view['compose_url'] = $compose_url; - return $view; - } - - $board = get_term($board_id, 'aksrt_forum_board'); - - if (!$board || is_wp_error($board)) { - return [ - 'board' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'application' => null, - 'board_options' => [], - 'tabs' => $tabs, - 'active_sort' => $orderby, - 'top_info' => $top_info, - 'compose_url' => function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : home_url('/'), - ]; - } - - $topics = $this->forum->get_topic_list([ - 'board_id' => $board_id, - 'page' => $page, - 'limit' => 20, - 'status' => 'publish', - 'viewer_user_id' => $user_id, - 'orderby' => $orderby, - ]); - - $sticky_topics = $this->forum->get_sticky_topics_for_board($board_id); - $board_link = get_term_link($board); - - return [ - 'board' => [ - 'id' => $board_id, - 'name' => (string) $board->name, - 'description' => (string) $board->description, - 'content' => '', - 'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true), - 'topic_count' => (int) $board->count, - 'reply_count' => 0, - 'today_reply_count' => 0, - 'views' => 0, - 'follow_count' => 0, - 'last_reply' => '', - 'last_post' => '', - 'url' => !is_wp_error($board_link) ? (string) $board_link : '', - 'thumbnail_url' => '', - 'author_id' => 0, - 'allow_view' => 'public', - ], - 'topics' => $topics['items'], - 'sticky_topics' => array_slice($sticky_topics, 0, 5), - 'moderators' => $this->forum->get_board_moderator_profiles($board_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0, - 'can_apply_moderator' => $user_id > 0 && !$this->forum->is_board_moderator($user_id, $board_id), - 'can_moderate' => $this->forum->is_board_moderator($user_id, $board_id), - ], - 'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $board_id) : null, - 'board_options' => $this->get_boards(), - 'tabs' => $tabs, - 'active_sort' => $orderby, - 'top_info' => $top_info, - 'compose_url' => $compose_url, - ]; - } - - /** - * Plate CPT board view (backwards-compatible with get_board_view return shape). - */ - private function get_plate_view_compat(int $plate_id, int $user_id, int $page, string $orderby = 'date'): array { - $plate = $this->get_plate($plate_id); - if (!$plate) { - return [ - 'board' => null, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => [], - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [], - 'application' => null, - 'board_options' => [], - ]; - } - - // Check access permission - $allow_view = $plate['allow_view'] ?? 'public'; - if (!$this->can_view_plate($user_id, $plate_id, $allow_view)) { - return [ - 'board' => $plate, - 'topics' => [], - 'sticky_topics' => [], - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0], - 'permissions' => [ - 'can_compose' => false, - 'can_apply_moderator' => false, - 'can_moderate' => false, - 'access_denied' => true, - 'access_reason' => $allow_view, - ], - 'application' => null, - 'board_options' => $this->get_boards(), - ]; - } - - $topics = $this->forum->get_topic_list([ - 'plate_id' => $plate_id, - 'page' => $page, - 'limit' => 20, - 'status' => 'publish', - 'viewer_user_id' => $user_id, - 'orderby' => $orderby, - ]); - - $sticky_topics = $this->get_sticky_topics_for_plate($plate_id); - - return [ - 'board' => $plate, - 'topics' => $topics['items'], - 'sticky_topics' => array_slice($sticky_topics, 0, 5), - 'moderators' => $this->get_plate_moderator_profiles($plate_id), - 'pagination' => [ - 'page' => $topics['page'], - 'max_pages' => $topics['max_pages'], - 'total' => $topics['total'], - ], - 'permissions' => [ - 'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id), - 'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'), - 'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id), - 'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id), - 'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id), - 'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id), - ], - 'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $plate_id) : null, - 'board_options' => $this->get_boards(), - ]; - } - - /** - * Check if a user can view a plate (§7.2 板块阅读权限). - */ - public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool { - if ($allow_view === '') { - $allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'); - } - - if ($allow_view === 'public') { - return true; - } - - if ($user_id <= 0) { - // Guest cash follow unlock via signed cookie (money follow pay only). - if ($allow_view === 'follow' - && function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id) - ) { - return true; - } - return false; - } - - if ($this->is_plate_moderator($user_id, $plate_id)) { - return true; - } - - if ($allow_view === 'login') { - return true; - } - - if ($allow_view === 'follow') { - return $this->is_user_following_plate($user_id, $plate_id); - } - - if ($allow_view === 'role') { - $allowed_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true); - if (!is_array($allowed_roles) || empty($allowed_roles)) { - return true; - } - $user = get_userdata($user_id); - if (!$user) { - return false; - } - return !empty(array_intersect($user->roles, $allowed_roles)); - } - - return true; - } - - /** - * Check if a user can compose in a plate (§7.2 板块发帖限制). - */ - public function can_compose_in_plate(int $user_id, int $plate_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_plate_moderator($user_id, $plate_id)) { - return true; - } - - $add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'); - if ($add_limit === 'all' || $add_limit === 'login') { - return true; - } - - // Moderators already returned true above. - if ($add_limit === 'moderator') { - return false; - } - - $user = get_userdata($user_id); - if (!$user) { - return false; - } - - if ($add_limit === 'role') { - $allowed_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); - if (!is_array($allowed_roles) || empty($allowed_roles)) { - return true; - } - return !empty(array_intersect($user->roles, $allowed_roles)); - } - - // Unknown contract → deny (no legacy fallthrough). - return false; - } - - /** - * Check if a user is following a plate (§7.2 关注). - */ - public function is_user_following_plate(int $user_id, int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id); - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - return false; - } - return in_array($plate_id, $follow_list, true); - } - - /** - * Toggle user follow on a plate (§7.2 关注/取消关注). - * - * When plate has paid follow enabled and user is not following, returns - * require_pay payload instead of free-following. - * - * @return array{followed?:bool,follow_count?:int,require_pay?:bool,pay?:array,message?:string,success?:bool} - */ - public function toggle_follow_plate(int $user_id, int $plate_id): array { - if ($user_id <= 0) { - return ['followed' => false, 'follow_count' => 0, 'success' => false, 'message' => __('请先登录', 'aksrt')]; - } - - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - - $followed = in_array($plate_id, $follow_list, true); - if ($followed) { - // Unfollow is always free (zibll warns but still allows cancel). - $follow_list = array_values(array_diff($follow_list, [$plate_id])); - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - return [ - 'followed' => false, - 'follow_count' => $count, - 'success' => true, - ]; - } - - // Follow path — paid gate. - if ($this->is_plate_follow_pay_required($plate_id, $user_id)) { - $price = $this->resolve_plate_follow_pay_price($plate_id, $user_id); - return [ - 'followed' => false, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'require_pay' => true, - 'success' => false, - 'message' => __('关注该版块需要付费', 'aksrt'), - 'pay' => $price, - ]; - } - - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - - return [ - 'followed' => true, - 'follow_count' => $count, - 'success' => true, - ]; - } - - /** - * Whether global+plate paid follow is on and user still needs to pay. - */ - public function is_plate_follow_pay_required(int $plate_id, int $user_id): bool { - if ($user_id <= 0 || $plate_id <= 0) { - return false; - } - if ($this->is_user_following_plate($user_id, $plate_id)) { - return false; - } - if ($this->is_plate_moderator($user_id, $plate_id) || user_can($user_id, 'manage_options')) { - return false; - } - if (!$this->is_plate_follow_pay_enabled($plate_id)) { - return false; - } - // Already purchased plate_follow order. - if (class_exists('\AKSRT\Payment\OrderRepository') && class_exists('\AKSRT\Core\DomainModel')) { - if (\AKSRT\Payment\OrderRepository::get_instance()->has_paid_access( - $user_id, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - $plate_id - )) { - return false; - } - } - return true; - } - - public function is_plate_follow_pay_enabled(int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - // Global switch from forum settings (SPA). - $global_on = true; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) { - $cfg = \AKSRT\Settings\Settings::get_forum_settings(); - // Nested permissions.plate_follow_pay or flat. - if (isset($cfg['permissions']['plate_follow_pay'])) { - $global_on = !empty($cfg['permissions']['plate_follow_pay']); - } elseif (isset($cfg['plate_follow_pay'])) { - $global_on = !empty($cfg['plate_follow_pay']); - } - } - if (!$global_on) { - return false; - } - if ((int) get_post_meta($plate_id, '_aksrt_follow_pay', true) !== 1) { - return false; - } - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if ($modo === 'points') { - return (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true) > 0; - } - return (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true) > 0; - } - - /** - * @return array{amount:float|int,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float|int,currency:string,vip_level:int,vip_set:bool} - */ - public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array { - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - - $is_points = $modo === 'points'; - $base = $is_points - ? max(0, (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true)) - : max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)); - - $vip_level = 0; - if ($user_id > 0) { - if (function_exists('aksrt_get_user_vip_level')) { - $vip_level = (int) aksrt_get_user_vip_level($user_id); - } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { - $vip_level = 2; - } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { - $vip_level = 1; - } - if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { - $vip_level = 1; - } - } - - $pick = static function ($raw) use ($is_points) { - if ($raw === '' || $raw === null || $raw === false) { - return null; - } - if (!is_numeric($raw)) { - return null; - } - return $is_points ? max(0, (int) $raw) : max(0.0, (float) $raw); - }; - - $vip2 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true)); - $vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true)); - if ($vip1 === null) { - // Legacy single VIP key → vip1. - $vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true)); - } - - $amount = $base; - $is_vip_price = false; - $vip_set = ($vip1 !== null || $vip2 !== null); - - if ($vip_level >= 2 && $vip2 !== null) { - $amount = $vip2; - $is_vip_price = true; - } elseif ($vip_level >= 1 && $vip1 !== null) { - $amount = $vip1; - $is_vip_price = true; - } elseif ($vip_level >= 2 && $vip1 !== null) { - // VIP2 user falls back to VIP1 when VIP2 unset. - $amount = $vip1; - $is_vip_price = true; - } - - if ($is_vip_price) { - return [ - 'amount' => $amount, - 'modo' => $modo, - 'is_vip_price' => true, - 'is_free' => $amount <= 0, - 'label' => $amount <= 0 - ? __('VIP 免费关注', 'aksrt') - : ($is_points - ? sprintf(__('VIP %d 积分', 'aksrt'), (int) $amount) - : sprintf(__('VIP ¥%s', 'aksrt'), number_format((float) $amount, 2, '.', ''))), - 'base' => $base, - 'currency' => $is_points ? 'points' : 'CNY', - 'vip_level' => $vip_level, - 'vip_set' => true, - ]; - } - - return [ - 'amount' => $base, - 'modo' => $modo, - 'is_vip_price' => false, - 'is_free' => false, - 'label' => $is_points - ? sprintf(__('%d 积分', 'aksrt'), (int) $base) - : sprintf(__('¥%s', 'aksrt'), number_format((float) $base, 2, '.', '')), - 'base' => $base, - 'currency' => $is_points ? 'points' : 'CNY', - 'vip_level' => $vip_level, - 'vip_set' => $vip_set, - ]; - } - - /** - * Force-add plate to user follow list (after paid success). - */ - public function mark_plate_followed(int $user_id, int $plate_id): void { - if ($user_id <= 0 || $plate_id <= 0) { - return; - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - $count = $this->count_plate_followers($plate_id); - update_post_meta($plate_id, '_aksrt_follow_count', $count); - } - - /** - * Purchase plate follow with points or balance (creates plate_follow order when cash). - * - * @return array{success:bool,message:string,followed?:bool,follow_count?:int,order_number?:string} - */ - public function purchase_plate_follow(int $user_id, int $plate_id): array { - if ($user_id <= 0) { - return $this->forum->error(__('请先登录', 'aksrt')); - } - $post = get_post($plate_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return $this->forum->error(__('版块不存在', 'aksrt')); - } - if ($this->is_user_following_plate($user_id, $plate_id)) { - return $this->forum->success(__('已关注', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - ]); - } - if (!$this->is_plate_follow_pay_enabled($plate_id)) { - // Free follow fallback. - $this->mark_plate_followed($user_id, $plate_id); - return $this->forum->success(__('关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - ]); - } - - $price = $this->resolve_plate_follow_pay_price($plate_id, $user_id); - if (!empty($price['is_free'])) { - $this->mark_plate_followed($user_id, $plate_id); - return $this->forum->success(__('VIP 免费关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'free' => true, - ]); - } - - if (!class_exists('\AKSRT\Credits\Credits')) { - return $this->forum->error(__('积分系统不可用', 'aksrt')); - } - - $modo = (string) ($price['modo'] ?? 'money'); - $amount = $price['amount'] ?? 0; - - if ($modo === 'points') { - $cost = max(0, (int) $amount); - if ($cost <= 0) { - return $this->forum->error(__('积分价格未设置', 'aksrt')); - } - $balance = \AKSRT\Credits\Credits::get_balance($user_id); - if ($balance < $cost) { - return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost)); - } - $deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'forum_plate_follow', [ - 'plate_id' => $plate_id, - ], sprintf(__('付费关注版块「%s」', 'aksrt'), get_the_title($plate_id))); - if (is_wp_error($deducted) || $deducted === false) { - $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt'); - return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt')); - } - $this->mark_plate_followed($user_id, $plate_id); - $this->grant_plate_follow_author_share($user_id, $plate_id, $cost, true); - return $this->forum->success(__('关注成功', 'aksrt'), [ - 'followed' => true, - 'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true), - 'credits_cost' => $cost, - ]); - } - - // Cash: multi-gateway cashier only (PlateFollowPay REST). No Credits×100 bypass. - return $this->forum->error(__('请通过收银台支付关注费用。', 'aksrt'), [ - 'require_cashier' => true, - 'pay' => $price, - 'modo' => 'money', - 'plate_id' => $plate_id, - ]); - } - - private function grant_plate_follow_author_share(int $buyer_id, int $plate_id, int $cost, bool $is_points): void { - if (!$is_points || $cost <= 0 || !class_exists('\AKSRT\Credits\Credits')) { - return; - } - $post = get_post($plate_id); - if (!$post) { - return; - } - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $buyer_id) { - return; - } - $author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70)); - $author_amount = (int) floor($cost * $author_share / 100); - if ($author_amount <= 0) { - return; - } - \AKSRT\Credits\Credits::add($author_id, $author_amount, 'forum_plate_follow_income', [ - 'plate_id' => $plate_id, - 'buyer_id' => $buyer_id, - ], sprintf(__('版块「%s」被付费关注', 'aksrt'), get_the_title($plate_id))); - } - - /** - * Count followers for a plate. - */ - private function count_plate_followers(int $plate_id): int { - global $wpdb; - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->usermeta} - WHERE meta_key = 'aksrt_follow_plate' - AND meta_value LIKE %s", - '%i:' . $plate_id . ';%' - )); - } - - /** - * Get plates a user follows. - */ - public function get_user_followed_plates(int $user_id): array { - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list) || empty($follow_list)) { - return []; - } - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post__in' => array_map('intval', $follow_list), - 'post_status' => 'publish', - 'posts_per_page' => 100, - 'orderby' => 'post__in', - ]); - - $result = []; - foreach ($plates as $plate) { - $result[] = $this->build_plate_payload($plate); - } - return $result; - } - - /** - * Get a user's managed plates (plates where user is moderator/author). - * - * @return array{owned:array,moderating:array} - */ - public function get_user_managed_plates(int $user_id): array { - $owned = []; - $moderating = []; - - if (!$this->forum->is_plate_cpt_active()) { - return ['owned' => $owned, 'moderating' => $moderating]; - } - - // Plates where user is author (超级版主) - $own_plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'author' => $user_id, - 'post_status' => 'publish', - 'posts_per_page' => 50, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - foreach ($own_plates as $plate) { - $owned[] = $this->build_plate_payload($plate); - } - - // Plates where user is a moderator (in _aksrt_moderators list) - global $wpdb; - $mod_plate_ids = $wpdb->get_col($wpdb->prepare( - "SELECT pm.post_id FROM {$wpdb->postmeta} pm - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'aksrt_forum_plate' AND p.post_status = 'publish' - WHERE pm.meta_key = %s", - '_aksrt_moderators' - )); - - foreach ($mod_plate_ids as $plate_id) { - $plate_id = (int) $plate_id; - // Skip plates already in owned - if (in_array($plate_id, wp_list_pluck($own_plates, 'ID'), true)) { - continue; - } - $mods = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - $plate = get_post($plate_id); - if ($plate) { - $moderating[] = $this->build_plate_payload($plate); - } - } - } - - return ['owned' => $owned, 'moderating' => $moderating]; - } - - // ── Plate utilities (extracted from facade) ── - - public function increment_plate_views(int $plate_id): void { - if ($plate_id <= 0 || !$this->forum->is_plate_cpt_active()) { - return; - } - $views = (int) get_post_meta($plate_id, '_aksrt_views', true); - update_post_meta($plate_id, '_aksrt_views', $views + 1); - } - - /** - * Check if a plate qualifies as "hot" (§7.2 热门版块). - */ - public function is_hot_plate(int $plate_id): bool { - $config = $this->forum->get_config(); - $threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100); - $posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true); - $follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true); - $is_hot = ($posts_count + $follow_count * 2) >= $threshold; - if ($is_hot && get_post_meta($plate_id, '_aksrt_level_hot_awarded', true) !== '1') { - $plate = get_post($plate_id); - $author_id = $plate instanceof \WP_Post ? (int) $plate->post_author : 0; - update_post_meta($plate_id, '_aksrt_level_hot_awarded', '1'); - do_action('aksrt_forum_plate_hot', $plate_id, $author_id); - } - return $is_hot; - } - - /** - * Get plate categories (分区) with moderators. - */ - public function get_plate_categories(): array { - $terms = get_terms([ - 'taxonomy' => 'aksrt_plate_cat', - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ]); - - if (is_wp_error($terms) || empty($terms)) { - return []; - } - - $categories = []; - foreach ($terms as $term) { - $term_id = (int) $term->term_id; - $moderator_ids = get_term_meta($term_id, '_aksrt_cat_moderator', true); - if (!is_array($moderator_ids)) { - $moderator_ids = []; - } - - $categories[] = [ - 'id' => $term_id, - 'name' => (string) $term->name, - 'description' => (string) $term->description, - 'slug' => (string) $term->slug, - 'count' => (int) $term->count, - 'parent_id' => (int) $term->parent, - 'moderators' => array_values(array_unique(array_filter(array_map('intval', $moderator_ids)))), - 'add_limit' => (string) (get_term_meta($term_id, '_aksrt_add_limit', true) ?: 'all'), - 'url' => (string) get_term_link($term), - ]; - } - - return $categories; - } -} diff --git a/src/AKSRT/Forum/BoardServicePart3.php b/src/AKSRT/Forum/BoardServicePart3.php deleted file mode 100644 index 85c97f97..00000000 --- a/src/AKSRT/Forum/BoardServicePart3.php +++ /dev/null @@ -1,187 +0,0 @@ -forum->is_forum_admin($user_id)) { - return true; - } - $moderator_ids = get_term_meta($cat_id, '_aksrt_cat_moderator', true); - if (!is_array($moderator_ids)) { - return false; - } - return in_array($user_id, array_map('intval', $moderator_ids), true); - } - - /** - * Get plates sorted by a specified dimension (§7.2 多维排序). - */ - public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array { - $allowed = [ - 'name' => 'title', - 'date' => 'date', - 'modified' => 'modified', - 'menu_order' => 'menu_order', - 'last_post' => '_aksrt_last_post', - 'last_reply' => '_aksrt_last_reply', - 'posts_count' => '_aksrt_posts_count', - 'reply_count' => '_aksrt_reply_count', - 'today_reply_count' => '_aksrt_today_reply_count', - 'views' => '_aksrt_views', - 'follow_count' => '_aksrt_follow_count', - 'rand' => 'rand', - ]; - - $query_args = [ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => $limit, - 'order' => strtoupper($order) === 'ASC' ? 'ASC' : 'DESC', - ]; - - if (isset($allowed[$orderby])) { - $meta_key = $allowed[$orderby]; - if (str_starts_with($meta_key, '_aksrt_')) { - $query_args['orderby'] = 'meta_value_num'; - $query_args['meta_key'] = $meta_key; - } else { - $query_args['orderby'] = $meta_key; - } - } else { - $query_args['orderby'] = 'menu_order'; - } - - $posts = get_posts($query_args); - $plates = []; - foreach ($posts as $post) { - $plates[] = $this->build_plate_payload($post); - } - return $plates; - } - - public function maybe_migrate_boards(): void { - if (get_option('aksrt_forum_boards_migrated')) { - return; - } - - $old_boards = get_option('aksrt_forum_boards', []); - if (!is_array($old_boards) || empty($old_boards)) { - update_option('aksrt_forum_boards_migrated', time()); - return; - } - - foreach ($old_boards as $board) { - $name = sanitize_text_field((string) ($board['name'] ?? '')); - if ($name === '') { - continue; - } - - $existing = get_terms([ - 'taxonomy' => 'aksrt_forum_board', - 'name' => $name, - 'hide_empty' => false, - 'fields' => 'ids', - ]); - - if (!is_wp_error($existing) && !empty($existing)) { - $term_id = (int) $existing[0]; - } else { - $result = wp_insert_term($name, 'aksrt_forum_board', [ - 'description' => sanitize_textarea_field((string) ($board['description'] ?? '')), - ]); - if (is_wp_error($result)) { - continue; - } - $term_id = (int) $result['term_id']; - } - - $icon = sanitize_text_field((string) ($board['icon'] ?? '')); - if ($icon !== '') { - update_term_meta($term_id, '_aksrt_icon', $icon); - } - - update_term_meta($term_id, '_aksrt_sort_order', (int) ($board['sort_order'] ?? $term_id)); - - if (!empty($board['moderators']) && is_array($board['moderators'])) { - update_term_meta($term_id, '_aksrt_moderators', array_map('intval', $board['moderators'])); - } - } - - update_option('aksrt_forum_boards_migrated', time()); - } - - public function ajax_follow_plate(): void { - check_ajax_referer('aksrt_forum_action', '_nonce'); - - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $plate_id = (int) ($_POST['plate_id'] ?? 0); - if ($plate_id <= 0) { - wp_send_json_error(['message' => __('版块不存在', 'aksrt')]); - } - - $result = $this->toggle_follow_plate($user_id, $plate_id); - if (!empty($result['require_pay'])) { - wp_send_json_error([ - 'code' => 'require_pay', - 'message' => (string) ($result['message'] ?? __('需要付费关注', 'aksrt')), - 'require_pay' => true, - 'pay' => $result['pay'] ?? [], - 'followed' => false, - 'follow_count' => (int) ($result['follow_count'] ?? 0), - ]); - } - if (isset($result['success']) && $result['success'] === false) { - wp_send_json_error([ - 'message' => (string) ($result['message'] ?? __('操作失败', 'aksrt')), - ]); - } - wp_send_json_success($result); - } - - public function ajax_purchase_plate_follow(): void { - check_ajax_referer('aksrt_forum_action', '_nonce'); - - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $plate_id = (int) ($_POST['plate_id'] ?? 0); - if ($plate_id <= 0) { - wp_send_json_error(['message' => __('版块不存在', 'aksrt')]); - } - - $result = $this->purchase_plate_follow($user_id, $plate_id); - if (empty($result['success'])) { - $err = [ - 'message' => (string) ($result['message'] ?? __('支付失败', 'aksrt')), - ]; - if (!empty($result['require_cashier'])) { - $err['require_cashier'] = true; - $err['pay'] = $result['pay'] ?? []; - $err['modo'] = (string) ($result['modo'] ?? 'money'); - $err['plate_id'] = (int) ($result['plate_id'] ?? $plate_id); - } - wp_send_json_error($err); - } - wp_send_json_success($result); - } -} diff --git a/src/AKSRT/Forum/CommentService.php b/src/AKSRT/Forum/CommentService.php deleted file mode 100644 index eacae583..00000000 --- a/src/AKSRT/Forum/CommentService.php +++ /dev/null @@ -1,604 +0,0 @@ -forum = $forum; - } - - // ── Hook: forum comment posted ─────────────────────────────────── - - public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void { - $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; - $commenter_id = (int) $comment->user_id; - if ($commenter_id <= 0) return; - - // Topic-level last_reply for board sort tabs. - update_post_meta((int) $post->ID, '_aksrt_last_reply', (string) $comment->comment_date); - - $this->forum->notify_topic_reply((int) $post->ID, $commenter_id); - } - - // ── AJAX: get forum comments ───────────────────────────────────── - - /** - * AJAX: Get forum comments with sort/filter/pagination. - */ - public function ajax_forum_comments(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $topic_id = (int) ($_POST['topic_id'] ?? 0); - $sort = sanitize_key((string) ($_POST['sort'] ?? 'newest')); - $sort = in_array($sort, ['newest', 'oldest', 'hottest'], true) ? $sort : 'newest'; - $author_only = !empty($_POST['author_only']); - $page = max(1, (int) ($_POST['page'] ?? 1)); - $per_page = max(1, min(100, (int) ($_POST['per_page'] ?? 30))); - - if ($topic_id <= 0) { - wp_send_json_error(['message' => __('参数无效', 'aksrt')]); - } - - $result = $this->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page); - - ob_start(); - $this->render_forum_comments($result['comments'], $topic_id, $result['adopted_id']); - $html = ob_get_clean(); - - wp_send_json_success([ - 'html' => $html, - 'total' => $result['total'], - 'max_pages' => $result['max_pages'], - 'page' => $result['page'], - 'has_more' => $result['page'] < $result['max_pages'], - ]); - } - - // ── AJAX: comment topping ──────────────────────────────────────── - - /** - * AJAX: Set comment topping level (moderator action). - */ - public function ajax_comment_topping(): void { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $comment_id = (int) ($_POST['comment_id'] ?? 0); - $level = max(0, min(3, (int) ($_POST['level'] ?? 0))); - $user_id = get_current_user_id(); - - $comment = get_comment($comment_id); - if (!$comment) { - wp_send_json_error(['message' => __('评论不存在', 'aksrt')]); - } - - $topic_id = (int) $comment->comment_post_ID; - if (!$this->forum->forum_user_can($user_id, 'reply_top', $topic_id)) { - wp_send_json_error(['message' => __('权限不足', 'aksrt')]); - } - - if ($this->set_comment_topping($comment_id, $level)) { - wp_send_json_success([ - 'message' => $level > 0 ? __('已置顶', 'aksrt') : __('已取消置顶', 'aksrt'), - 'level' => $level, - ]); - } - - wp_send_json_error(['message' => __('操作失败', 'aksrt')]); - } - - // ── Render forum comments ──────────────────────────────────────── - - /** - * Render forum comments HTML for AJAX and initial load. - * - * @param \WP_Comment[] $comments Comments (parents + children mixed). - * @param int $topic_id Topic post ID. - * @param int $adopted_id Adopted comment ID (0 if none). - */ - public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void { - $post = get_post($topic_id); - $post_author_id = $post ? (int) $post->post_author : 0; - $current_user_id = get_current_user_id(); - $is_question = 'question' === (string) get_post_meta($topic_id, '_aksrt_bbs_type', true); - $can_adopt = $is_question && $current_user_id > 0 - && ($current_user_id === $post_author_id || $this->forum->forum_user_can($current_user_id, 'reply_adopt_other', $topic_id)); - $can_moderate = $this->forum->forum_user_can($current_user_id, 'topic_moderate', $topic_id); - - if (empty($comments)) { - echo '
    ' . esc_html__('暂无回复,来抢沙发吧!', 'aksrt') . '
    '; - return; - } - - foreach ($comments as $comment) { - if (!($comment instanceof \WP_Comment)) continue; - if ((int) $comment->comment_parent !== 0) continue; - - $comment_id = (int) $comment->comment_ID; - $commenter_uid = (int) $comment->user_id; - $is_adopted = $comment_id === $adopted_id; - $is_hot = $this->is_hot_comment($comment_id); - $is_post_author = $commenter_uid > 0 && $commenter_uid === $post_author_id; - $topping = (int) get_comment_meta($comment_id, '_aksrt_comment_topping', true); - $badge = $this->get_commenter_badge($commenter_uid, $topic_id); - - $like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0; - $is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($comment_id) : false; - - $classes = ['aksrt-forum-reply']; - if ($is_adopted) $classes[] = 'is-adopted'; - if ($is_hot) $classes[] = 'is-hot'; - if ($topping > 0) $classes[] = 'is-pinned'; - ?> -
    -
    -
    -
    - - - - - - - - 0) : ?> - - - - - - - - - -
    - -
    -
    - - - - - - - - - - - - - -
    - - - comment_parent === $comment_id; - }); - if (!empty($child_comments)) : ?> -
    - comment_ID; - $child_uid = (int) $child->user_id; - $child_badge = $this->get_commenter_badge($child_uid, $topic_id); - $child_is_author = $child_uid > 0 && $child_uid === $post_author_id; - $child_like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($child_id) : 0; - $child_is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($child_id) : false; - $parent_comment = get_comment((int) $child->comment_parent); - $parent_name = $parent_comment ? get_comment_author((int) $parent_comment->comment_ID) : ''; - ?> -
    -
    -
    -
    - - - - - - - - comment_parent !== $comment_id) : ?> - - - -
    -
    -
    - - - - -
    -
    -
    - -
    - -
    -
    - post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); - if ($bbs_type !== 'question') { - return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); - } - - $author_id = (int) $post->post_author; - if ($operator_id !== $author_id && !$this->forum->can_moderate_topic($operator_id, $topic_id)) { - return new \WP_Error('forbidden', __('仅提问者或版主可采纳答案', 'aksrt')); - } - - $comment = get_comment($comment_id); - if (!$comment || (int) $comment->comment_post_ID !== $topic_id) { - return new \WP_Error('invalid_comment', __('回复不存在', 'aksrt')); - } - - $existing = get_post_meta($topic_id, '_aksrt_adopted_comment', true); - if ($existing && (int) $existing === $comment_id) { - return new \WP_Error('already_adopted', __('该回复已被采纳', 'aksrt')); - } - - if ($existing) { - delete_comment_meta((int) $existing, '_aksrt_adopted'); - } - - update_comment_meta($comment_id, '_aksrt_adopted', '1'); - update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id); - do_action('aksrt_forum_reply_adopted', $topic_id, $comment_id, (int) $comment->user_id); - update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED - - $adoptee_id = (int) $comment->user_id; - if ($adoptee_id > 0) { - $config = $this->forum->get_config(); - $amount = (int) ($config['credits']['adopted'] ?? 20); - if ($amount > 0) { - $already = get_comment_meta($comment_id, '_aksrt_adopt_credited', true); - if (!$already) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - $granted = \AKSRT\Credits\PointsFree::grant($adoptee_id, 'bbs_adopt', $amount, [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - if (!is_wp_error($granted) && (int) $granted > 0) { - update_comment_meta($comment_id, '_aksrt_adopt_credited', '1'); - } - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($adoptee_id, $amount, 'forum_adopted', [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - update_comment_meta($comment_id, '_aksrt_adopt_credited', '1'); - } - } - } - - $this->forum->notify($adoptee_id, 'system', sprintf( - __('你的回复被采纳为最佳答案(帖子「%s」)', 'aksrt'), - get_the_title($topic_id) - ), [ - 'title' => __('回复被采纳', 'aksrt'), - 'target_url' => get_permalink($topic_id) . '#comment-' . $comment_id, - ]); - } - - return [ - 'message' => __('已采纳该回复为最佳答案', 'aksrt'), - 'comment_id' => $comment_id, - 'solved' => true, - ]; - } - - - - public function get_adopted_comment_id(int $topic_id): int { - return (int) get_post_meta($topic_id, '_aksrt_adopted_comment', true); - } - - // ── Get forum comments (query) ────────────────────────────────── - - /** - * Get forum comments with sort/filter support. - * - * @param int $topic_id Topic post ID. - * @param string $sort 'newest'|'oldest'|'hottest' - * @param bool $author_only Only show post-author replies. - * @param int $page Page number. - * @param int $per_page Items per page. - * @return array{comments:\WP_Comment[],total:int,max_pages:int,page:int,adopted_id:int} - */ - public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return ['comments' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1, 'adopted_id' => 0]; - } - - $args = [ - 'post_id' => $topic_id, - 'status' => 'approve', - 'parent' => 0, - 'number' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]; - - if ($author_only) { - $args['user_id'] = (int) $post->post_author; - } - - if ($sort === 'hottest') { - $args['meta_key'] = 'aksrt_comment_likes'; - $args['orderby'] = 'meta_value_num'; - $args['order'] = 'DESC'; - } elseif ($sort === 'oldest') { - $args['orderby'] = 'comment_date_gmt'; - $args['order'] = 'ASC'; - } else { - $args['orderby'] = 'comment_date_gmt'; - $args['order'] = 'DESC'; - } - - // Total count for pagination - $count_args = $args; - unset($count_args['number'], $count_args['offset']); - $count_args['count'] = true; - $total = (int) get_comments($count_args); - - $top_level = get_comments($args); - - - // Fetch child comments for these parents - if (!empty($top_level)) { - $parent_ids = wp_list_pluck($top_level, 'comment_ID'); - $children = get_comments([ - 'post_id' => $topic_id, - 'status' => 'approve', - 'parent__in' => $parent_ids, - 'orderby' => 'comment_date_gmt', - 'order' => 'ASC', - 'number' => 200, - ]); - } else { - $children = []; - } - - // Float pinned top-level comments - $pinned = []; - $normal = []; - foreach ($top_level as $c) { - $topping = (int) get_comment_meta((int) $c->comment_ID, '_aksrt_comment_topping', true); - if ($topping > 0) { - $c->aksrt_topping = $topping; - $pinned[] = $c; - } else { - $normal[] = $c; - } - } - usort($pinned, function ($a, $b) { - return ($b->aksrt_topping ?? 0) <=> ($a->aksrt_topping ?? 0); - }); - $comments = array_merge($pinned, $normal, $children); - - return [ - 'comments' => $comments, - 'total' => $total, - 'max_pages' => max(1, (int) ceil($total / $per_page)), - 'page' => $page, - 'adopted_id' => $this->get_adopted_comment_id($topic_id), - ]; - } - - // ── Set comment topping ───────────────────────────────────────── - - /** - * Set comment topping level. - * - * @param int $comment_id Comment ID. - * @param int $level 0=unpin, 1=normal pin, 2=hot pin, 3=best answer pin. - */ - public function set_comment_topping(int $comment_id, int $level): bool { - $comment = get_comment($comment_id); - if (!$comment) { - return false; - } - if ($level <= 0) { - delete_comment_meta($comment_id, '_aksrt_comment_topping'); - delete_comment_meta($comment_id, '_aksrt_comment_pinned'); - } else { - update_comment_meta($comment_id, '_aksrt_comment_topping', $level); - update_comment_meta($comment_id, '_aksrt_comment_pinned', '1'); - } - return true; - } - - - - // ── Maybe mark hot comment ────────────────────────────────────── - - /** - * Check and mark a comment as hot ("神评") if it crosses the like threshold. - * - * @return bool True if newly marked as hot. - */ - public function maybe_mark_hot_comment(int $comment_id): bool { - $comment = get_comment($comment_id); - if (!$comment || '1' !== (string) $comment->comment_approved) { - return false; - } - - $post = get_post((int) $comment->comment_post_ID); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return false; - } - - $already_hot = '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); - if ($already_hot) { - return false; - } - - $config = $this->forum->get_config(); - $threshold = max(1, (int) ($config['reply']['hot_threshold'] ?? 10)); - $likes = max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); - - if ($likes < $threshold) { - return false; - } - - update_comment_meta($comment_id, '_aksrt_is_hot', '1'); - $hot_author = (int) (get_comment($comment_id)->user_id ?? 0); - do_action('aksrt_forum_comment_hot', $comment_id, $hot_author); - - // Credit the comment author (free-task day cap). - $author_id = (int) $comment->user_id; - if ($author_id > 0) { - $amount = (int) ($config['credits']['comment_hot'] ?? $config['credits']['comment_like'] ?? 5); - if ($amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_comment_hot', $amount, [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_comment', [ - 'type' => 'comment', - 'id' => $comment_id, - ]); - } - } - } - - // Notify the author - if ($author_id > 0) { - $this->forum->notify($author_id, 'system', sprintf( - __('你的回复成为了神评(帖子「%s」)', 'aksrt'), - get_the_title((int) $comment->comment_post_ID) - ), [ - 'title' => __('恭喜获得神评', 'aksrt'), - 'target_url' => get_permalink((int) $comment->comment_post_ID) . '#comment-' . $comment_id, - ]); - } - - return true; - } - - // ── Is hot comment ────────────────────────────────────────────── - - /** - * Check if a comment is a hot comment ("神评"). - */ - public function is_hot_comment(int $comment_id): bool { - return '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); - } - - // ── Get commenter badge ───────────────────────────────────────── - - /** - * Get the user's moderator tier label for badge rendering in comments. - * - * @return array{tier:int,label:string,class:string}|null - */ - public function get_commenter_badge(int $user_id, int $topic_id): ?array { - if ($user_id <= 0) { - return null; - } - - if ($this->forum->is_forum_admin($user_id)) { - return [ - 'tier' => Forum::TIER_FORUM_ADMIN, - 'label' => __('管理员', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--admin', - ]; - } - - $plate_id = $this->forum->get_topic_plate_id($topic_id); - if ($plate_id > 0) { - $tier = $this->forum->get_user_plate_tier($user_id, $plate_id); - if ($tier >= Forum::TIER_CAT_MOD) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['cat_moderator'] ?? __('分区版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--catmod', - ]; - } - if ($tier >= Forum::TIER_PLATE_AUTHOR) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['plate_author'] ?? __('超级版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--super', - ]; - } - if ($tier >= Forum::TIER_MODERATOR) { - $names = $this->forum->get_config()['names'] ?? []; - return [ - 'tier' => $tier, - 'label' => $names['plate_moderator'] ?? __('版主', 'aksrt'), - 'class' => 'aksrt-forum-reply__role-badge--mod', - ]; - } - } - - return null; - } -} diff --git a/src/AKSRT/Forum/DraftService.php b/src/AKSRT/Forum/DraftService.php deleted file mode 100644 index 8d70e894..00000000 --- a/src/AKSRT/Forum/DraftService.php +++ /dev/null @@ -1,441 +0,0 @@ -forum = $forum; - } - - public function save_draft(int $user_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $draft_id = (int) ($data['draft_id'] ?? 0); - - $post_arr = [ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'draft', - 'post_author' => $user_id, - 'post_title' => sanitize_text_field((string) ($data['title'] ?? '')), - 'post_content' => wp_kses_post((string) ($data['content'] ?? '')), - ]; - - if ($draft_id > 0) { - $existing = get_post($draft_id); - if (!$existing || $existing->post_type !== 'aksrt_forum_topic' - || (int) $existing->post_author !== $user_id - || $existing->post_status !== 'draft') { - return new \WP_Error('invalid_draft', __('草稿不存在或无权编辑', 'aksrt')); - } - $post_arr['ID'] = $draft_id; - $result = wp_update_post($post_arr, true); - } else { - $result = wp_insert_post($post_arr, true); - } - - if (is_wp_error($result)) { - return $result; - } - - $post_id = (int) $result; - - $board_id = (int) ($data['board_id'] ?? 0); - if ($board_id > 0) { - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($post_id, '_aksrt_plate_id', $board_id); - } else { - wp_set_object_terms($post_id, [$board_id], 'aksrt_forum_board'); - } - } - - $tags = is_array($data['tags'] ?? null) ? $data['tags'] : []; - if (!empty($tags)) { - $clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5); - wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag'); - } - - $meta_fields = [ - 'visibility' => '_aksrt_visibility', - 'bbs_type' => '_aksrt_bbs_type', - 'special_topic' => '_aksrt_special_topic', - 'cover_type' => '_aksrt_cover_type', - ]; - foreach ($meta_fields as $key => $meta_key) { - if (isset($data[$key])) { - update_post_meta($post_id, $meta_key, sanitize_key((string) $data[$key])); - } - } - - if (isset($data['credits_cost'])) { - $cost = max(0, (int) $data['credits_cost']); - update_post_meta($post_id, '_aksrt_credits_cost', $cost); - update_post_meta($post_id, '_aksrt_pay_credits', $cost); - } - if (isset($data['pay_price'])) { - update_post_meta($post_id, '_aksrt_pay_price', sanitize_text_field((string) $data['pay_price'])); - } - if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1'])); - } - if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2'])); - } - if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') { - $v1 = max(0, (float) $data['pay_vip1_price']); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1); - update_post_meta($post_id, '_aksrt_pay_vip_price', $v1); - } - if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price'])); - } - if (array_key_exists('pay_hide_part', $data)) { - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - } - if (array_key_exists('topic_coupon_s', $data)) { - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0); - } - if (isset($data['visibility_roles']) && is_array($data['visibility_roles'])) { - update_post_meta($post_id, '_aksrt_visibility_roles', array_map('sanitize_key', $data['visibility_roles'])); - } - - $this->forum->save_cover_meta($post_id, $data); - - if (!empty($data['vote_enabled'])) { - $vote_meta = $this->forum->prepare_vote_meta($data); - $this->forum->save_vote_meta($post_id, $vote_meta); - } - - if (isset($data['topic_cat'])) { - $topic_cat = sanitize_text_field((string) $data['topic_cat']); - if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) { - wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat'); - } - } - - return [ - 'post_id' => $post_id, - 'message' => __('草稿已保存', 'aksrt'), - ]; - } - - public function get_user_draft_count(int $user_id): int { - if ($user_id <= 0) { - return 0; - } - global $wpdb; - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_forum_topic' AND post_status = 'draft' AND post_author = %d", - $user_id - )); - } - - /** - * @return array - */ - public function get_user_drafts(int $user_id, int $limit = 20): array { - if ($user_id <= 0) { - return []; - } - - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'draft', - 'author' => $user_id, - 'posts_per_page' => $limit, - 'orderby' => 'modified', - 'order' => 'DESC', - 'no_found_rows' => true, - ]); - - $drafts = []; - while ($query->have_posts()) { - $query->the_post(); - $post = get_post(); - if (!$post) { - continue; - } - - $board_name = ''; - if ($this->forum->is_plate_cpt_active()) { - $plate_id = (int) get_post_meta($post->ID, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $board_name = get_the_title($plate_id); - } - } else { - $terms = wp_get_post_terms($post->ID, 'aksrt_forum_board', ['fields' => 'names']); - if (!is_wp_error($terms) && !empty($terms)) { - $board_name = $terms[0]; - } - } - - $drafts[] = [ - 'id' => $post->ID, - 'title' => $post->post_title ?: __('(无标题)', 'aksrt'), - 'date' => $post->post_modified, - 'board_name' => $board_name, - ]; - } - wp_reset_postdata(); - - return $drafts; - } - - public function delete_draft(int $user_id, int $draft_id): bool { - if ($user_id <= 0 || $draft_id <= 0) { - return false; - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return false; - } - return (bool) wp_delete_post($draft_id, true); - } - - /** - * Load a draft's full data for the compose form. - */ - public function load_draft_data(int $user_id, int $draft_id): ?array { - if ($user_id <= 0 || $draft_id <= 0) { - return null; - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return null; - } - - $board_id = 0; - if ($this->forum->is_plate_cpt_active()) { - $board_id = (int) get_post_meta($draft_id, '_aksrt_plate_id', true); - } else { - $terms = wp_get_post_terms($draft_id, 'aksrt_forum_board', ['fields' => 'ids']); - if (!is_wp_error($terms) && !empty($terms)) { - $board_id = (int) $terms[0]; - } - } - - $tags = wp_get_post_terms($draft_id, 'aksrt_forum_tag', ['fields' => 'names']); - if (is_wp_error($tags)) { - $tags = []; - } - - $topic_cat = ''; - if (taxonomy_exists('aksrt_forum_topic_cat')) { - $cats = wp_get_post_terms($draft_id, 'aksrt_forum_topic_cat', ['fields' => 'names']); - if (!is_wp_error($cats) && !empty($cats)) { - $topic_cat = $cats[0]; - } - } - - $slide_raw = get_post_meta($draft_id, '_aksrt_featured_slide', true); - - return [ - 'draft_id' => $draft_id, - 'title' => $post->post_title, - 'content' => $post->post_content, - 'board_id' => $board_id, - 'tags' => implode(', ', $tags), - 'visibility' => get_post_meta($draft_id, '_aksrt_visibility', true) ?: Forum::VISIBILITY_PUBLIC, - 'bbs_type' => get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD, - 'special_topic' => get_post_meta($draft_id, '_aksrt_special_topic', true) ?: '', - 'cover_type' => get_post_meta($draft_id, '_aksrt_cover_type', true) ?: '', - 'cover_image' => get_post_meta($draft_id, '_aksrt_cover_image', true) ?: '', - 'featured_slide' => is_array($slide_raw) ? $slide_raw : [], - 'featured_video' => get_post_meta($draft_id, '_aksrt_featured_video', true) ?: '', - 'credits_cost' => (int) get_post_meta($draft_id, '_aksrt_credits_cost', true), - 'pay_price' => get_post_meta($draft_id, '_aksrt_pay_price', true) ?: '', - 'credits_vip1' => get_post_meta($draft_id, '_aksrt_credits_vip1', true), - 'credits_vip2' => get_post_meta($draft_id, '_aksrt_credits_vip2', true), - 'pay_vip1_price' => get_post_meta($draft_id, '_aksrt_pay_vip1_price', true), - 'pay_vip2_price' => get_post_meta($draft_id, '_aksrt_pay_vip2_price', true), - 'pay_hide_part' => (int) get_post_meta($draft_id, '_aksrt_pay_hide_part', true) === 1, - 'topic_coupon_s' => (int) get_post_meta($draft_id, '_aksrt_topic_coupon_s', true) === 1, - 'visibility_roles' => get_post_meta($draft_id, '_aksrt_visibility_roles', true) ?: [], - 'topic_cat' => $topic_cat, - 'vote_enabled' => get_post_meta($draft_id, '_aksrt_vote_enabled', true) === '1', - 'vote_title' => get_post_meta($draft_id, '_aksrt_vote_title', true) ?: '', - 'vote_multiple' => get_post_meta($draft_id, '_aksrt_vote_multiple', true) === '1', - 'vote_type' => get_post_meta($draft_id, '_aksrt_vote_type', true) ?: 'standard', - 'vote_expiry_days' => (int) get_post_meta($draft_id, '_aksrt_vote_expiry_days', true), - 'vote_options' => $this->get_vote_options_text($draft_id), - ]; - } - - private function get_vote_options_text(int $post_id): string { - $options = get_post_meta($post_id, '_aksrt_vote_options', true); - if (!is_array($options)) { - return ''; - } - return implode("\n", array_column($options, 'label')); - } - - // ────────────────────────────────────────────────────────── - // AJAX handlers - // ────────────────────────────────────────────────────────── - - public function ajax_save_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $data = [ - 'draft_id' => (int) ($_POST['draft_id'] ?? 0), - 'board_id' => (int) ($_POST['board_id'] ?? 0), - 'title' => sanitize_text_field((string) ($_POST['title'] ?? '')), - 'content' => wp_kses_post((string) ($_POST['content'] ?? '')), - 'tags' => $this->forum->split_tags((string) ($_POST['tags'] ?? '')), - 'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')), - 'visibility' => sanitize_key((string) ($_POST['visibility'] ?? Forum::VISIBILITY_PUBLIC)), - 'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? Forum::BBS_TYPE_STANDARD)), - 'topic_cat' => sanitize_text_field((string) ($_POST['topic_cat'] ?? '')), - 'vote_enabled' => !empty($_POST['vote_enabled']), - 'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')), - 'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')), - 'vote_multiple' => !empty($_POST['vote_multiple']), - 'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')), - 'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0), - 'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')), - 'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')), - 'featured_slide' => (string) ($_POST['featured_slide'] ?? ''), - 'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')), - 'credits_cost' => (int) ($_POST['credits_cost'] ?? 0), - 'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')), - 'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '', - 'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '', - 'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '', - 'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '', - 'pay_hide_part' => !empty($_POST['pay_hide_part']), - 'topic_coupon_s' => !empty($_POST['topic_coupon_s']), - 'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles']) - ? array_map('sanitize_key', $_POST['visibility_roles']) : [], - ]; - - $result = $this->save_draft($user_id, $data); - if (is_wp_error($result)) { - wp_send_json_error(['message' => $result->get_error_message()]); - } - - wp_send_json_success($result); - } - - public function ajax_load_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - $draft_id = (int) ($_GET['draft_id'] ?? $_POST['draft_id'] ?? 0); - - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - $data = $this->load_draft_data($user_id, $draft_id); - if (!$data) { - wp_send_json_error(['message' => __('草稿不存在', 'aksrt')]); - } - - wp_send_json_success($data); - } - - public function ajax_delete_draft(): void { - check_ajax_referer('aksrt_forum_compose', '_nonce'); - $user_id = get_current_user_id(); - $draft_id = (int) ($_POST['draft_id'] ?? 0); - - if ($user_id <= 0) { - wp_send_json_error(['message' => __('请先登录', 'aksrt')]); - } - - if (!$this->delete_draft($user_id, $draft_id)) { - wp_send_json_error(['message' => __('删除失败', 'aksrt')]); - } - - wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]); - } - - // ────────────────────────────────────────────────────────── - // Publish draft (transition from draft → publish/pending) - // ────────────────────────────────────────────────────────── - - public function publish_draft(int $user_id, int $draft_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - $post = get_post($draft_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic' - || (int) $post->post_author !== $user_id - || $post->post_status !== 'draft') { - return new \WP_Error('invalid_draft', __('草稿不存在', 'aksrt')); - } - - $data['draft_id'] = $draft_id; - $save_result = $this->save_draft($user_id, $data); - if (is_wp_error($save_result)) { - return $save_result; - } - - $board_id = (int) ($data['board_id'] ?? 0); - $title = sanitize_text_field((string) ($data['title'] ?? '')); - $content = wp_kses_post((string) ($data['content'] ?? '')); - - if ($board_id <= 0 || $title === '' || $content === '') { - return new \WP_Error('invalid_payload', __('请填写版块、标题和内容', 'aksrt')); - } - - $can_publish = $this->forum->can_user_publish_directly($user_id, $board_id); - $post_status = $can_publish ? 'publish' : 'pending'; - $review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING; - - wp_update_post([ - 'ID' => $draft_id, - 'post_status' => $post_status, - ]); - - update_post_meta($draft_id, '_aksrt_review_status', $review_status); - update_post_meta($draft_id, '_aksrt_sticky', Forum::STICKY_NONE); - update_post_meta($draft_id, '_aksrt_featured', '0'); - update_post_meta($draft_id, '_aksrt_locked', '0'); - update_post_meta($draft_id, '_aksrt_views', 0); - update_post_meta($draft_id, '_aksrt_last_reply', (string) get_post_field('post_date', $draft_id)); - - $bbs_type = get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD; - if ($bbs_type === Forum::BBS_TYPE_QUESTION) { - update_post_meta($draft_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED); - } - - if ($post_status === 'publish') { - $this->forum->sync_board_count($board_id); - if (class_exists('\AKSRT\Credits\Earn')) { - do_action('aksrt_forum_topic_created', $user_id, $draft_id); - } - } - - if ($post_status === 'pending') { - $this->forum->notify_topic_pending($draft_id, $user_id); - } - - return [ - 'post_id' => $draft_id, - 'message' => $post_status === 'publish' - ? __('主题已发布', 'aksrt') - : __('主题已提交审核,审核通过后对外展示', 'aksrt'), - 'redirect' => $post_status === 'publish' - ? get_permalink($draft_id) - : add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()), - ]; - } -} diff --git a/src/AKSRT/Forum/Forum.php b/src/AKSRT/Forum/Forum.php deleted file mode 100644 index 1d9b8fae..00000000 --- a/src/AKSRT/Forum/Forum.php +++ /dev/null @@ -1,959 +0,0 @@ -perm_service) { - $this->perm_service = new PermissionService($this); - } - return $this->perm_service; - } - - private function score_service(): ScoreService { - if (!$this->score_service) { - $this->score_service = new ScoreService($this); - } - return $this->score_service; - } - - private function comment_service(): CommentService { - if (!$this->comment_service) { - $this->comment_service = new CommentService($this); - } - return $this->comment_service; - } - - private function draft_service(): DraftService { - if (!$this->draft_svc) { - // Composer PSR-4: AKSRT\Forum\DraftService → src/AKSRT/Forum/DraftService.php - // Never require legacy class-aksrt-forum-draft-service.php. - if (!class_exists(DraftService::class, true)) { - throw new \RuntimeException( - 'AKSRT\\Forum\\DraftService missing; deploy src/AKSRT/Forum/DraftService.php and run composer dump-autoload if needed' - ); - } - $this->draft_svc = new DraftService($this); - } - return $this->draft_svc; - } - - private ?TopicService $topic_svc = null; - - private ?ModerationService $mod_svc = null; - private function moderation_service(): ModerationService { - if ($this->mod_svc === null) { - $this->mod_svc = new ModerationService($this); - } - return $this->mod_svc; - } - - private ?ViewBuilder $view_builder = null; - private function view_builder(): ViewBuilder { - if ($this->view_builder === null) { - $this->view_builder = new ViewBuilder($this); - } - return $this->view_builder; - } - - private function topic_service(): TopicService { - if ($this->topic_svc === null) { - $this->topic_svc = new TopicService($this); - } - return $this->topic_svc; - } - private function board_service(): BoardService { - if (!$this->board_svc) { - $this->board_svc = new BoardService($this); - } - return $this->board_svc; - } - - public function is_plate_cpt_active(): bool { - // Plate CPT is the native board model; no option migration gate. - return post_type_exists('aksrt_forum_plate'); - } - - public function get_config(): array { - if (class_exists('\AKSRT\Settings\Settings')) { - return \AKSRT\Settings\Settings::get_forum_settings(); - } - - return ['enabled' => false]; - } - - /** - * Alias kept for topic-service helpers that still call get_forum_config(). - */ - public function get_forum_config(): array { - return $this->get_config(); - } - - public function get_boards(array $args = []): array { - return $this->board_service()->get_boards($args); - } - - public function create_board(array $data): int { - return $this->board_service()->create_board($data); - } - - public function update_board(int $board_id, array $data): bool { - return $this->board_service()->update_board($board_id, $data); - } - - public function delete_board(int $board_id): bool { - return $this->board_service()->delete_board($board_id); - } - - public function reorder_boards(array $order_map): bool { - return $this->board_service()->reorder_boards($order_map); - } - - // §7.0 Plate CPT methods —板块 CPT 化 CRUD - - public function get_plates(array $args = []): array { - return $this->board_service()->get_plates($args); - } - - public function get_plate(int $plate_id): ?array { - return $this->board_service()->get_plate($plate_id); - } - - public function build_plate_payload(\WP_Post $post): array { - return $this->board_service()->build_plate_payload($post); - } - - public function create_plate(array $data): int { - return $this->board_service()->create_plate($data); - } - - public function update_plate(int $plate_id, array $data): bool { - return $this->board_service()->update_plate($plate_id, $data); - } - - public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool { - return $this->board_service()->delete_plate($plate_id, $cascade, $move_to); - } - - public function reorder_plates(array $order_map): bool { - return $this->board_service()->reorder_plates($order_map); - } - - public function get_topic_plate_id(int $topic_id): int { - return $this->board_service()->get_topic_plate_id($topic_id); - } - - public function is_plate_moderator(int $user_id, int $plate_id): bool { - return $this->board_service()->is_plate_moderator($user_id, $plate_id); - } - - public function get_plate_view(int $plate_id, array $args = []): array { - return $this->board_service()->get_plate_view($plate_id, $args); - } - - public function get_topics(array $args = []): array { - $view = $this->get_topic_list($args); - return $view['items']; - } - - public function get_topic_list(array $args = []): array { - return $this->view_builder()->get_topic_list($args); - } - - public function get_home_view(int $user_id = 0): array { - return $this->view_builder()->get_home_view($user_id); - } - - public function get_follow_stream(int $user_id, int $page = 1, int $limit = 15): array { - return $this->view_builder()->get_follow_stream($user_id, $page, $limit); - } - - public function get_synthesis_stream(int $page = 1, int $limit = 15, int $viewer = 0): array { - return $this->view_builder()->get_synthesis_stream($page, $limit, $viewer); - } - - public function get_pending_stream(int $user_id, int $page = 1, int $limit = 15): array { - return $this->view_builder()->get_pending_stream($user_id, $page, $limit); - } - - public function ajax_home_tab(): void { - $this->view_builder()->ajax_home_tab(); - } - - public function ajax_board_topics(): void { - $this->view_builder()->ajax_board_topics(); - } - - public function register_widget_routes(): void { - register_rest_route('aksrt/v1', '/forum/frontend/widgets/topics', [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_widget_topics'], - 'permission_callback' => '__return_true', - ]); - register_rest_route('aksrt/v1', '/forum/frontend/widgets/plates', [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_widget_plates'], - 'permission_callback' => '__return_true', - ]); - } - - public function rest_widget_topics(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) { - return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500); - } - $instance = [ - 'count' => max(1, min(30, (int) ($request->get_param('count') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')), - 'plate_id' => max(0, (int) ($request->get_param('plate_id') ?? 0)), - 'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), - ]; - $page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1)); - $payload = \AKSRT\Widgets\WidgetForumTopicLists::query_payload($instance, $page); - return new \WP_REST_Response([ - 'success' => true, - 'html' => (string) ($payload['html'] ?? ''), - 'paginate_html' => (string) ($payload['paginate_html'] ?? ''), - 'has_more' => !empty($payload['has_more']), - 'page' => (int) ($payload['page'] ?? $page), - 'max_pages' => (int) ($payload['max_pages'] ?? 1), - 'total' => (int) ($payload['total'] ?? 0), - ], 200); - } - - public function rest_widget_plates(\WP_REST_Request $request) { - if (!class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) { - return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500); - } - $instance = [ - 'showposts' => max(1, min(30, (int) ($request->get_param('showposts') ?? $request->get_param('count') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')), - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')), - 'filter' => sanitize_key((string) ($request->get_param('filter') ?? '')), - 'cat' => sanitize_text_field((string) ($request->get_param('cat') ?? '')), - ]; - $payload = \AKSRT\Widgets\WidgetForumPlateLists::query_payload($instance); - return new \WP_REST_Response([ - 'success' => true, - 'html' => (string) ($payload['html'] ?? ''), - ], 200); - } - - - - public function render_board_cards(array $boards): void { - $this->view_builder()->render_board_cards($boards); - } - - public function render_topic_row(array $topic, string $style = 'detail'): void { - $this->view_builder()->render_topic_row($topic, $style); - } - - public function get_board_view(int $board_id, array $args = []): array { - return $this->board_service()->get_board_view($board_id, $args); - } - - public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool { - return $this->board_service()->can_view_plate($user_id, $plate_id, $allow_view); - } - - public function can_compose_in_plate(int $user_id, int $plate_id): bool { - return $this->board_service()->can_compose_in_plate($user_id, $plate_id); - } - - public function is_user_following_plate(int $user_id, int $plate_id): bool { - return $this->board_service()->is_user_following_plate($user_id, $plate_id); - } - - public function is_plate_follow_pay_enabled(int $plate_id): bool { - return $this->board_service()->is_plate_follow_pay_enabled($plate_id); - } - - public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array { - return $this->board_service()->resolve_plate_follow_pay_price($plate_id, $user_id); - } - - public function mark_plate_followed(int $user_id, int $plate_id): void { - $this->board_service()->mark_plate_followed($user_id, $plate_id); - } - - public function toggle_follow_plate(int $user_id, int $plate_id): array { - return $this->board_service()->toggle_follow_plate($user_id, $plate_id); - } - - public function get_user_followed_plates(int $user_id): array { - return $this->board_service()->get_user_followed_plates($user_id); - } - - public function get_user_managed_plates(int $user_id): array { - return $this->board_service()->get_user_managed_plates($user_id); - } - - public function get_user_forum_topics(int $user_id, array $args = []): array { - return $this->view_builder()->get_user_forum_topics($user_id, $args); - } - - public function get_user_favorite_topics(int $user_id, int $page = 1, int $limit = 10): array { - return $this->view_builder()->get_user_favorite_topics($user_id, $page, $limit); - } - - public function get_user_forum_stats(int $user_id): array { - return $this->view_builder()->get_user_forum_stats($user_id); - } - - public function increment_plate_views(int $plate_id): void { - $this->board_service()->increment_plate_views($plate_id); - } - - public function is_hot_plate(int $plate_id): bool { - return $this->board_service()->is_hot_plate($plate_id); - } - - public function get_plate_categories(): array { - return $this->board_service()->get_plate_categories(); - } - - public function is_cat_moderator(int $user_id, int $cat_id): bool { - return $this->board_service()->is_cat_moderator($user_id, $cat_id); - } - - public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array { - return $this->board_service()->get_sorted_plates($orderby, $order, $limit); - } - - public function get_topic_view(int $topic_id, int $user_id = 0): array { - return $this->view_builder()->get_topic_view($topic_id, $user_id); - } - - public function get_compose_view(int $user_id, array $args = []): array { - return $this->view_builder()->get_compose_view($user_id, $args); - } - - public function get_stats(): array { - return $this->view_builder()->get_stats(); - } - - public function can_edit_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->can_edit_topic($user_id, $topic_id); - } - - public function can_moderate_topic(int $user_id, int $topic_id): bool { - return $this->forum_user_can($user_id, 'topic_moderate', $topic_id); - } - - public function update_topic(int $topic_id, array $data) { - return $this->topic_service()->update_topic($topic_id, $data); - } - - public function get_tags(): array { - return $this->view_builder()->get_tags(); - } - - public function get_tag_archive_view(array $args = []): array { - return $this->view_builder()->get_tag_archive_view($args); - } - - public function init(): void { - add_action('wp_ajax_aksrt_forum_update_topic', [$this, 'ajax_update_topic']); - add_action('wp_ajax_aksrt_forum_topic_list', [$this, 'ajax_topic_list']); - add_action('wp_ajax_aksrt_forum_create_topic', [$this, 'ajax_create_topic']); - add_action('wp_ajax_aksrt_forum_follow_plate', [$this, 'ajax_follow_plate']); - add_action('wp_ajax_aksrt_forum_purchase_plate_follow', [$this, 'ajax_purchase_plate_follow']); - add_action('wp_ajax_aksrt_forum_unlock_topic', [$this, 'ajax_unlock_topic']); - - // §7.4 draft persistence - add_action('wp_ajax_aksrt_forum_save_draft', [$this, 'ajax_save_draft']); - add_action('wp_ajax_aksrt_forum_load_draft', [$this, 'ajax_load_draft']); - add_action('wp_ajax_aksrt_forum_delete_draft', [$this, 'ajax_delete_draft']); - - // §7.4 remote search selectors - add_action('wp_ajax_aksrt_forum_search_boards', [$this, 'ajax_search_boards']); - add_action('wp_ajax_nopriv_aksrt_forum_search_boards', [$this, 'ajax_search_boards']); - add_action('wp_ajax_aksrt_forum_search_tags', [$this, 'ajax_search_tags']); - add_action('wp_ajax_nopriv_aksrt_forum_search_tags', [$this, 'ajax_search_tags']); - add_action('wp_ajax_aksrt_forum_search_topics', [$this, 'ajax_search_topics']); - add_action('wp_ajax_nopriv_aksrt_forum_search_topics', [$this, 'ajax_search_topics']); - - // §7.4 AJAX-based topic editing - add_action('wp_ajax_aksrt_forum_edit_topic', [$this, 'ajax_edit_topic']); - - // §7.6 forum comment engine - add_action('wp_ajax_aksrt_forum_comments', [$this, 'ajax_forum_comments']); - add_action('wp_ajax_nopriv_aksrt_forum_comments', [$this, 'ajax_forum_comments']); - add_action('wp_ajax_aksrt_forum_comment_top', [$this, 'ajax_comment_topping']); - - self::register_hide_shortcode(); - - // §7.10 forum home tab AJAX - add_action('wp_ajax_aksrt_forum_home_tab', [$this, 'ajax_home_tab']); - add_action('wp_ajax_nopriv_aksrt_forum_home_tab', [$this, 'ajax_home_tab']); - - // Public plate/board topic list (sort tabs) - add_action('wp_ajax_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); - add_action('wp_ajax_nopriv_aksrt_forum_board_topics', [$this, 'ajax_board_topics']); - add_action('rest_api_init', [$this, 'register_widget_routes']); - - add_action('admin_init', [$this, 'maybe_migrate_boards'], 25); - add_filter('template_include', [$this, 'template_routing']); - add_action('wp', [$this, 'maybe_refresh_recommend_on_view']); - add_action('comment_post', [$this, 'on_forum_comment_posted'], 10, 3); - - // §7.2 板块多维统计 hooks - add_action('save_post_aksrt_forum_topic', [$this, 'on_topic_saved_sync_plate'], 20, 2); - add_action('comment_post', [$this, 'on_comment_sync_plate_reply'], 25, 3); - - add_action('wp_login', [$this, 'cap_cache_on_login'], 10, 2); - add_action('updated_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('added_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('deleted_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3); - add_action('updated_term_meta', [$this, 'cap_cache_on_cat_meta'], 10, 3); - add_action('save_post_aksrt_forum_plate', [$this, 'cap_cache_on_plate_save'], 10, 1); - } - - public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void { - $this->moderation_service()->on_topic_saved_sync_plate($post_id, $post); - } - - public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void { - $this->moderation_service()->on_comment_sync_plate_reply($comment_id, $approved, $commentdata); - } - - public function template_routing(string $template): string { - return $this->view_builder()->template_routing($template); - } - - public function update_topic_meta(int $topic_id, array $meta): bool { - return $this->topic_service()->update_topic_meta($topic_id, $meta); - } - - public function is_moderator(int $user_id, int $topic_id): bool { - return $this->moderation_service()->is_moderator($user_id, $topic_id); - } - - public function ajax_update_topic(): void { - $this->topic_service()->ajax_update_topic(); - } - - public function ajax_topic_list(): void { - $this->view_builder()->ajax_topic_list(); - } - - public function ajax_create_topic(): void { - $this->topic_service()->ajax_create_topic(); - } - - public function create_topic(int $user_id, array $data) { - return $this->topic_service()->create_topic($user_id, $data); - } - - public function moderate_topic(int $topic_id, string $action, array $payload, int $operator_id = 0) { - return $this->topic_service()->moderate_topic($topic_id, $action, $payload, $operator_id); - } - - public function submit_vote(int $topic_id, int $user_id, array $option_ids) { - return $this->topic_service()->submit_vote($topic_id, $user_id, $option_ids); - } - - public function ajax_follow_plate(): void { - $this->board_service()->ajax_follow_plate(); - } - - public function ajax_purchase_plate_follow(): void { - $this->board_service()->ajax_purchase_plate_follow(); - } - - public function ajax_unlock_topic(): void { - $this->topic_service()->ajax_unlock_topic(); - } - - /** - * Lightweight result helper for topic service actions. - * - * @return array{success:bool,message:string} - */ - public function success(string $message = '', array $extra = []): array { - return array_merge(['success' => true, 'message' => $message], $extra); - } - - /** - * @return array{success:bool,message:string} - */ - public function error(string $message = '', array $extra = []): array { - return array_merge(['success' => false, 'message' => $message], $extra); - } - - public function apply_moderator(int $user_id, int $board_id, string $reason) { - return $this->moderation_service()->apply_moderator($user_id, $board_id, $reason); - } - - public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) { - return $this->moderation_service()->review_moderator_application($application_id, $status, $note, $reviewer_id); - } - - public function get_moderator_application_list(array $args = []): array { - return $this->moderation_service()->get_moderator_application_list($args); - } - - public function sync_board_count(int $board_id): void { - $this->moderation_service()->sync_board_count($board_id); - } - - public function sync_plate_counts(int $plate_id): void { - $this->moderation_service()->sync_plate_counts($plate_id); - } - - public function get_board_moderators(int $board_id): array { - return $this->moderation_service()->get_board_moderators($board_id); - } - - public function get_plate_moderators(int $plate_id): array { - return $this->moderation_service()->get_plate_moderators($plate_id); - } - - public function is_forum_admin(int $user_id): bool { - return $this->moderation_service()->is_forum_admin($user_id); - } - - public function is_board_moderator(int $user_id, int $board_id): bool { - return $this->moderation_service()->is_board_moderator($user_id, $board_id); - } - - public function get_board_moderator_profiles(int $board_id): array { - return $this->moderation_service()->get_board_moderator_profiles($board_id); - } - - public function get_board_moderator_profiles_with_roles(int $board_id): array { - return $this->moderation_service()->get_board_moderator_profiles_with_roles($board_id); - } - - public function get_moderator_roles(int $board_id): array { - return $this->moderation_service()->get_moderator_roles($board_id); - } - - public function set_moderator_role(int $user_id, int $board_id, string $role): bool { - return $this->moderation_service()->set_moderator_role($user_id, $board_id, $role); - } - - public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool { - return $this->moderation_service()->add_moderator($user_id, $board_id, $role); - } - - public function remove_moderator(int $user_id, int $board_id): bool { - return $this->moderation_service()->remove_moderator($user_id, $board_id); - } - - public function get_user_moderator_boards(int $user_id): array { - return $this->moderation_service()->get_user_moderator_boards($user_id); - } - - public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool { - return $this->moderation_service()->has_moderator_role($user_id, $board_id, $min_role); - } - - public function get_board_moderator_applications(int $board_id): array { - return $this->moderation_service()->get_board_moderator_applications($board_id); - } - - public function get_plate_moderator_applications(int $plate_id): array { - return $this->moderation_service()->get_plate_moderator_applications($plate_id); - } - - public function get_board_application_for_user(int $user_id, int $board_id): ?array { - return $this->moderation_service()->get_board_application_for_user($user_id, $board_id); - } - - public function get_visibility_options(): array { - return $this->topic_service()->get_visibility_options(); - } - - public function get_review_options(): array { - return $this->topic_service()->get_review_options(); - } - - public function can_user_publish_directly(int $user_id, int $board_id = 0): bool { - return $this->topic_service()->can_user_publish_directly($user_id, $board_id); - } - - public function get_sticky_topics_for_board(int $board_id): array { - return $this->view_builder()->get_sticky_topics_for_board($board_id); - } - - public function get_topic_detail(int $topic_id, int $viewer_user_id = 0): array { - return $this->view_builder()->get_topic_detail($topic_id, $viewer_user_id); - } - - public function increment_view_count(int $topic_id): void { - $this->view_builder()->increment_view_count($topic_id); - } - - public function maybe_migrate_boards(): void { - $this->board_service()->maybe_migrate_boards(); - } - - public function build_topic_payload(\WP_Post $post, int $viewer_user_id = 0): array { - return $this->topic_service()->build_topic_payload($post, $viewer_user_id); - } - - public function unlock_topic_with_credits(int $user_id, int $topic_id): array { - return $this->topic_service()->unlock_topic_with_credits($user_id, $topic_id); - } - - public function has_user_unlocked_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->has_user_unlocked_topic($user_id, $topic_id); - } - - public function has_user_paid_topic(int $user_id, int $topic_id): bool { - return $this->topic_service()->has_user_paid_topic($user_id, $topic_id); - } - - public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array { - return $this->topic_service()->resolve_topic_pay_price($topic_id, $user_id); - } - - public function mark_topic_unlocked(int $user_id, int $topic_id): void { - $this->topic_service()->mark_topic_unlocked($user_id, $topic_id); - } - - public function prepare_vote_meta(array $data): array { - return $this->topic_service()->prepare_vote_meta($data); - } - - public function save_vote_meta(int $topic_id, array $vote_meta): void { - $this->topic_service()->save_vote_meta($topic_id, $vote_meta); - } - - public function normalize_post_status_query(string $status) { - return $this->topic_service()->normalize_post_status_query($status); - } - - private function build_user_summary(int $user_id): array { - return $this->topic_service()->build_user_summary($user_id); - } - - private function get_user_avatar_url(int $user_id): string { - return $this->topic_service()->get_user_avatar_url($user_id); - } - - public function save_cover_meta(int $post_id, array $data): void { - $this->topic_service()->save_cover_meta($post_id, $data); - } - - public function split_tags(string $tags): array { - return $this->topic_service()->split_tags($tags); - } - - private function get_visibility_map(): array { - return $this->topic_service()->get_visibility_map(); - } - - private function get_review_map(): array { - return $this->topic_service()->get_review_map(); - } - - private function get_visibility_label(string $visibility): string { - return $this->topic_service()->get_visibility_label($visibility); - } - - private function get_review_label(string $review_status): string { - return $this->topic_service()->get_review_label($review_status); - } - - public function is_favorited(int $user_id, int $topic_id): bool { - return $this->score_service()->is_favorited($user_id, $topic_id); - } - - public function toggle_favorite(int $user_id, int $topic_id) { - return $this->score_service()->toggle_favorite($user_id, $topic_id); - } - - public function score_topic(int $user_id, int $topic_id, int $amount) { - return $this->score_service()->score_topic($user_id, $topic_id, $amount); - } - - public function get_score_detail(int $topic_id): array { - return $this->score_service()->get_score_detail($topic_id); - } - - public function calculate_recommend_score(int $topic_id): float { - return $this->score_service()->calculate_recommend_score($topic_id); - } - - public function refresh_recommend_score(int $topic_id): float { - return $this->score_service()->refresh_recommend_score($topic_id); - } - - public function batch_refresh_recommend_scores(int $limit = 100): int { - return $this->score_service()->batch_refresh_recommend_scores($limit); - } - - public function maybe_refresh_recommend_on_view(): void { - $this->score_service()->maybe_refresh_recommend_on_view(); - } - - public function notify(int $user_id, string $type, string $content, array $data = []): void { - $this->score_service()->notify($user_id, $type, $content, $data); - } - - public function notify_topic_pending(int $topic_id, int $author_id): void { - $this->score_service()->notify_topic_pending($topic_id, $author_id); - } - - public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void { - $this->score_service()->notify_moderation_result($topic_id, $action, $operator_id); - } - - public function notify_topic_reply(int $topic_id, int $commenter_id): void { - $this->score_service()->notify_topic_reply($topic_id, $commenter_id); - } - - public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void { - $this->score_service()->notify_topic_scored($topic_id, $scorer_id, $amount); - } - - const COMMENT_TOP_NONE = 0; - const COMMENT_TOP_NORMAL = 1; - const COMMENT_TOP_HOT = 2; - const COMMENT_TOP_BEST = 3; - - public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void { - $this->score_service()->on_forum_comment_posted($comment_id, $approved, $commentdata); - } - - public function ajax_forum_comments(): void { - $this->comment_service()->ajax_forum_comments(); - } - - public function ajax_comment_topping(): void { - $this->comment_service()->ajax_comment_topping(); - } - - public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void { - $this->comment_service()->render_forum_comments($comments, $topic_id, $adopted_id); - } - - public function adopt_answer(int $topic_id, int $comment_id, int $operator_id) { - return $this->comment_service()->adopt_answer($topic_id, $comment_id, $operator_id); - } - - public function get_adopted_comment_id(int $topic_id): int { - return $this->comment_service()->get_adopted_comment_id($topic_id); - } - - public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array { - return $this->comment_service()->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page); - } - - public function set_comment_topping(int $comment_id, int $level): bool { - return $this->comment_service()->set_comment_topping($comment_id, $level); - } - - public function maybe_mark_hot_comment(int $comment_id): bool { - return $this->comment_service()->maybe_mark_hot_comment($comment_id); - } - - public function is_hot_comment(int $comment_id): bool { - return $this->comment_service()->is_hot_comment($comment_id); - } - - public function get_commenter_badge(int $user_id, int $topic_id): ?array { - return $this->comment_service()->get_commenter_badge($user_id, $topic_id); - } - - public function save_draft(int $user_id, array $data) { - return $this->draft_service()->save_draft($user_id, $data); - } - - public function get_user_draft_count(int $user_id): int { - return $this->draft_service()->get_user_draft_count($user_id); - } - - public function get_user_drafts(int $user_id, int $limit = 20): array { - return $this->draft_service()->get_user_drafts($user_id, $limit); - } - - public function delete_draft(int $user_id, int $draft_id): bool { - return $this->draft_service()->delete_draft($user_id, $draft_id); - } - - public function load_draft_data(int $user_id, int $draft_id): ?array { - return $this->draft_service()->load_draft_data($user_id, $draft_id); - } - - public function ajax_save_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_save_draft(); - }); - } - - public function ajax_load_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_load_draft(); - }); - } - - public function ajax_delete_draft(): void { - $this->run_draft_ajax(static function (self $forum): void { - $forum->draft_service()->ajax_delete_draft(); - }); - } - - /** - * Draft AJAX must never leak WP recovery HTML — return JSON on Throwable. - */ - private function run_draft_ajax(callable $callback): void { - try { - $callback($this); - } catch (\Throwable $e) { - if (defined('WP_DEBUG') && WP_DEBUG) { - error_log('[AKSRT forum draft ajax] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); - } - // Prefer JSON even when autoload/require still broken on a partial deploy. - if (!headers_sent()) { - status_header(500); - } - wp_send_json_error([ - 'message' => sprintf( - /* translators: %s: exception message */ - __('草稿操作失败:%s', 'aksrt'), - $e->getMessage() - ), - 'file' => basename($e->getFile()), - 'line' => $e->getLine(), - ], 500); - } - } - - public function ajax_search_boards(): void { - $this->view_builder()->ajax_search_boards(); - } - - public function ajax_search_tags(): void { - $this->view_builder()->ajax_search_tags(); - } - - public function ajax_search_topics(): void { - $this->view_builder()->ajax_search_topics(); - } - - public function ajax_edit_topic(): void { - $this->topic_service()->ajax_edit_topic(); - } - - public function get_compose_capabilities(int $user_id): array { - return $this->permission_service()->get_compose_capabilities($user_id); - } - - public function sanitize_topic_content(int $user_id, string $content): string { - return $this->topic_service()->sanitize_topic_content($user_id, $content); - } - - public static function register_hide_shortcode(): void { - add_shortcode('aksrt_hide', [TopicService::class, 'render_hide_shortcode']); - } - - public function publish_draft(int $user_id, int $draft_id, array $data) { - return $this->draft_service()->publish_draft($user_id, $draft_id, $data); - } - - // - // 四级层级(对标 zibll): - // TIER_FORUM_ADMIN (4) — bbs_admin_users + manage_options - // TIER_CAT_MOD (3) — aksrt_plate_cat term meta 'moderator' - // TIER_PLATE_AUTHOR(2) — plate CPT post_author(超级版主) - // TIER_MODERATOR (1) — plate CPT post meta '_aksrt_moderators' list - // - // 公开 API: - // forum_user_can( $uid, $cap, $context_id = 0 ): bool - // get_user_plate_tier( $uid, $plate_id ): int - // flush_cap_cache( $uid = 0 ): void - - public function get_user_plate_tier(int $user_id, int $plate_id): int { - return $this->permission_service()->get_user_plate_tier($user_id, $plate_id); - } - - public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool { - return $this->permission_service()->forum_user_can($user_id, $cap, $context_id); - } - - public function flush_cap_cache(int $user_id = 0): void { - $this->permission_service()->flush_cap_cache($user_id); - } - - public function cap_cache_on_login(string $user_login, \WP_User $user): void { - $this->permission_service()->cap_cache_on_login($user_login, $user); - } - - public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $this->permission_service()->cap_cache_on_plate_meta($meta_id, $object_id, $meta_key); - } - - public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $this->permission_service()->cap_cache_on_cat_meta($meta_id, $object_id, $meta_key); - } - - public function cap_cache_on_plate_save($post_id): void { - $this->permission_service()->cap_cache_on_plate_save($post_id); - } -} diff --git a/src/AKSRT/Forum/ModerationService.php b/src/AKSRT/Forum/ModerationService.php deleted file mode 100644 index d8ac9713..00000000 --- a/src/AKSRT/Forum/ModerationService.php +++ /dev/null @@ -1,717 +0,0 @@ -forum = $forum; - } - - public function apply_moderator(int $user_id, int $board_id, string $reason) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($board_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate') { - return new \WP_Error('invalid_board', __('版块不存在', 'aksrt')); - } - } else { - $board = get_term($board_id, 'aksrt_forum_board'); - if (!$board || is_wp_error($board)) { - return new \WP_Error('invalid_board', __('版块不存在', 'aksrt')); - } - } - - if ($this->is_board_moderator($user_id, $board_id)) { - return new \WP_Error('already_moderator', __('你已经是该版块版主', 'aksrt')); - } - - $applications = $this->get_board_moderator_applications($board_id); - foreach ($applications as $application) { - if ((int) ($application['user_id'] ?? 0) === $user_id && (string) ($application['status'] ?? '') === Forum::APPLICATION_PENDING) { - return new \WP_Error('application_exists', __('你已经提交过申请,等待处理即可', 'aksrt')); - } - } - - $reason = trim($reason); - if ($reason === '') { - return new \WP_Error('empty_reason', __('请填写申请理由', 'aksrt')); - } - - $application = [ - 'id' => wp_generate_uuid4(), - 'board_id' => $board_id, - 'user_id' => $user_id, - 'reason' => sanitize_textarea_field($reason), - 'status' => Forum::APPLICATION_PENDING, - 'created_at' => current_time('mysql'), - 'reviewed_at' => '', - 'review_note' => '', - 'reviewer_id' => 0, - ]; - - $applications[] = $application; - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($board_id, '_aksrt_moderator_applications', $applications); - } else { - update_term_meta($board_id, '_aksrt_moderator_applications', $applications); - } - - return [ - 'message' => __('版主申请已提交', 'aksrt'), - 'application' => $application, - ]; - } - - /** - * Review a moderator application. - * - * @return array|\WP_Error - */ - public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) { - if ($reviewer_id <= 0 || !$this->forum->forum_user_can($reviewer_id, 'moderator_review')) { - return new \WP_Error('forbidden', __('权限不足', 'aksrt')); - } - - if (!in_array($status, [Forum::APPLICATION_APPROVED, Forum::APPLICATION_REJECTED], true)) { - return new \WP_Error('invalid_status', __('申请处理状态无效', 'aksrt')); - } - - $use_cpt = $this->forum->is_plate_cpt_active(); - $boards = $this->forum->get_boards(); - $flat_boards = $this->flatten_boards($boards); - - foreach ($flat_boards as $board) { - $board_id = (int) ($board['id'] ?? 0); - $applications = $this->get_board_moderator_applications($board_id); - $updated = false; - - foreach ($applications as &$application) { - if ((string) ($application['id'] ?? '') !== $application_id) { - continue; - } - - $application['status'] = $status; - $application['review_note'] = sanitize_textarea_field($note); - $application['reviewed_at'] = current_time('mysql'); - $application['reviewer_id'] = $reviewer_id; - $updated = true; - - if ($status === Forum::APPLICATION_APPROVED) { - $this->add_moderator((int) ($application['user_id'] ?? 0), $board_id); - } - - break; - } - unset($application); - - if ($updated) { - if ($use_cpt) { - update_post_meta($board_id, '_aksrt_moderator_applications', $applications); - } else { - update_term_meta($board_id, '_aksrt_moderator_applications', $applications); - } - - $reviewed_application = $this->find_application_by_id($application_id); - if ($reviewed_application) { - $reviewed_user_id = (int) ($reviewed_application['user_id'] ?? 0); - if ($reviewed_user_id > 0 && $reviewed_user_id !== $reviewer_id) { - $board_name = (string) ($board['name'] ?? __('当前版块', 'aksrt')); - $status_label = $status === Forum::APPLICATION_APPROVED - ? __('已通过', 'aksrt') - : __('已拒绝', 'aksrt'); - $payload = [ - 'title' => __('版主申请结果', 'aksrt'), - 'target_url' => isset($board['url']) ? (string) $board['url'] : aksrt_get_forum_home_url(), - ]; - $clean_note = sanitize_textarea_field($note); - if ($clean_note !== '') { - $payload['reason'] = $clean_note; - } - $this->forum->notify( - $reviewed_user_id, - 'system', - sprintf(__('你在「%1$s」提交的版主申请%2$s', 'aksrt'), $board_name, $status_label), - $payload - ); - } - } - - return [ - 'message' => __('申请状态已更新', 'aksrt'), - 'application' => $reviewed_application, - ]; - } - } - - return new \WP_Error('application_not_found', __('申请记录不存在', 'aksrt')); - } - - /** - * Flatten hierarchical boards array (collapse children into flat list). - */ - private function flatten_boards(array $boards): array { - $flat = []; - foreach ($boards as $board) { - $children = $board['children'] ?? []; - unset($board['children']); - $flat[] = $board; - if (!empty($children)) { - $flat = array_merge($flat, $this->flatten_boards($children)); - } - } - return $flat; - } - - /** - * Get flattened moderator application list. - */ - public function get_moderator_application_list(array $args = []): array { - $status = sanitize_key((string) ($args['status'] ?? '')); - $board_id = (int) ($args['board_id'] ?? 0); - $keyword = sanitize_text_field((string) ($args['keyword'] ?? '')); - $items = []; - - foreach ($this->forum->get_boards() as $board) { - $current_board_id = (int) ($board['id'] ?? 0); - if ($board_id > 0 && $current_board_id !== $board_id) { - continue; - } - - foreach ($this->get_board_moderator_applications($current_board_id) as $application) { - if ($status !== '' && (string) ($application['status'] ?? '') !== $status) { - continue; - } - - $user = get_userdata((int) ($application['user_id'] ?? 0)); - $row = [ - 'id' => (string) ($application['id'] ?? ''), - 'board_id' => $current_board_id, - 'board_name' => (string) ($board['name'] ?? ''), - 'user_id' => (int) ($application['user_id'] ?? 0), - 'display_name' => $user ? $user->display_name : '', - 'user_email' => $user ? $user->user_email : '', - 'avatar_url' => $user ? $this->forum->get_user_avatar_url((int) $user->ID) : '', - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? ''), - 'status_label' => $this->get_application_status_label((string) ($application['status'] ?? '')), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer' => $this->forum->build_user_summary((int) ($application['reviewer_id'] ?? 0)), - ]; - - if ($keyword !== '') { - $haystack = strtolower(implode(' ', [ - (string) $row['board_name'], - (string) $row['display_name'], - (string) $row['user_email'], - (string) $row['reason'], - ])); - if (strpos($haystack, strtolower($keyword)) === false) { - continue; - } - } - - $items[] = $row; - } - } - - usort($items, static function ($left, $right): int { - return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); - }); - - return $items; - } - - /** - * Sync board/plate topic count + reply count. - */ - public function sync_board_count(int $board_id): void { - if ($board_id <= 0) { - return; - } - - if ($this->forum->is_plate_cpt_active()) { - $this->sync_plate_counts($board_id); - return; - } - - $count = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'posts_per_page' => 1, - 'fields' => 'ids', - 'tax_query' => [[ - 'taxonomy' => 'aksrt_forum_board', - 'field' => 'term_id', - 'terms' => $board_id, - ]], - 'post_status' => 'publish', - ]); - - update_term_meta($board_id, '_aksrt_topic_count', (int) $count->found_posts); - } - - /** - * Sync plate multi-dimensional counters (§7.2 去规范化统计). - */ - public function sync_plate_counts(int $plate_id): void { - global $wpdb; - - $posts_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->postmeta} pm - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'", - (string) $plate_id - )); - - $reply_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1'", - (string) $plate_id - )); - - $today_start = current_time('Y-m-d') . ' 00:00:00'; - $today_reply_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1' AND c.comment_date >= %s", - (string) $plate_id, - $today_start - )); - - $last_post = $wpdb->get_var($wpdb->prepare( - "SELECT p.post_date FROM {$wpdb->posts} p - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish' - ORDER BY p.post_date DESC LIMIT 1", - (string) $plate_id - )); - - $last_reply = $wpdb->get_var($wpdb->prepare( - "SELECT c.comment_date FROM {$wpdb->comments} c - INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID - WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s - AND c.comment_approved = '1' - ORDER BY c.comment_date DESC LIMIT 1", - (string) $plate_id - )); - - update_post_meta($plate_id, '_aksrt_posts_count', $posts_count); - update_post_meta($plate_id, '_aksrt_reply_count', $reply_count); - update_post_meta($plate_id, '_aksrt_today_reply_count', $today_reply_count); - update_post_meta($plate_id, '_aksrt_last_post', (string) ($last_post ?: '')); - update_post_meta($plate_id, '_aksrt_last_reply', (string) ($last_reply ?: '')); - } - - /** - * Get board moderator IDs (统一入口,自动检测 CPT/taxonomy). - */ - public function get_board_moderators(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - return $this->get_plate_moderators($board_id); - } - - // Fallback: taxonomy - $moderators = get_term_meta($board_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return []; - } - - return array_values(array_unique(array_filter(array_map('intval', $moderators)))); - } - - /** - * Get plate moderators from CPT meta. - */ - public function get_plate_moderators(int $plate_id): array { - if (!post_type_exists('aksrt_forum_plate')) { - return []; - } - - $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); - if (!is_array($moderators)) { - return []; - } - - return array_values(array_unique(array_filter(array_map('intval', $moderators)))); - } - - /** - * Check if a user is a forum-level admin (bbs_admin_users setting). - * - * Forum admins have cross-board governance privileges and bypass bans. - */ - public function is_forum_admin(int $user_id): bool { - if ($user_id <= 0) { - return false; - } - - if (user_can($user_id, 'manage_options')) { - return true; - } - - $forum = \AKSRT\Settings\Settings::get_forum_settings(); - $admin_users = $forum['admin_users'] ?? []; - - return in_array($user_id, $admin_users, true); - } - - /** - * Check if a user is a board moderator. - */ - public function is_board_moderator(int $user_id, int $board_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_forum_admin($user_id)) { - return true; - } - - return in_array($user_id, $this->get_board_moderators($board_id), true); - } - - /** - * Get board moderator profile rows. - */ - public function get_board_moderator_profiles(int $board_id): array { - $profiles = []; - foreach ($this->get_board_moderators($board_id) as $user_id) { - $user = get_userdata($user_id); - if (!$user) { - continue; - } - - $profiles[] = [ - 'id' => $user_id, - 'display_name' => $user->display_name, - 'user_email' => $user->user_email, - 'avatar_url' => $this->forum->get_user_avatar_url($user_id), - ]; - } - - return $profiles; - } - - /** - * Get board moderator profiles with role information. - */ - public function get_board_moderator_profiles_with_roles(int $board_id): array { - $roles_map = $this->get_moderator_roles($board_id); - $profiles = []; - foreach ($this->get_board_moderators($board_id) as $user_id) { - $user = get_userdata($user_id); - if (!$user) continue; - $profiles[] = [ - 'id' => $user_id, - 'display_name' => $user->display_name, - 'user_email' => $user->user_email, - 'avatar_url' => $this->forum->get_user_avatar_url($user_id), - 'role' => $roles_map[$user_id] ?? 'moderator', - ]; - } - return $profiles; - } - - /** - * Get moderator role map for a board: [user_id => role]. - * - * In CPT mode, also maps plate author → 'plate_author' (超级版主). - */ - public function get_moderator_roles(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - $roles = get_post_meta($board_id, '_aksrt_moderator_roles', true); - $roles = is_array($roles) ? $roles : []; - $post = get_post($board_id); - if ($post && $post->post_author > 0) { - $roles[(int) $post->post_author] = 'plate_author'; - } - return $roles; - } - $roles = get_term_meta($board_id, '_aksrt_moderator_roles', true); - return is_array($roles) ? $roles : []; - } - - /** - * Set a moderator's role on a board. - */ - public function set_moderator_role(int $user_id, int $board_id, string $role): bool { - $allowed = ['super_moderator', 'moderator', 'junior_moderator', 'plate_author', 'cat_moderator']; - if (!in_array($role, $allowed, true)) { - return false; - } - if (!in_array($user_id, $this->get_board_moderators($board_id), true)) { - return false; - } - $roles = $this->get_moderator_roles($board_id); - $roles[$user_id] = $role; - if ($this->forum->is_plate_cpt_active()) { - return (bool) update_post_meta($board_id, '_aksrt_moderator_roles', $roles); - } - return (bool) update_term_meta($board_id, '_aksrt_moderator_roles', $roles); - } - - /** - * Add a moderator to a board with optional role. - */ - public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool { - $current = $this->get_board_moderators($board_id); - if (in_array($user_id, $current, true)) { - $this->set_moderator_role($user_id, $board_id, $role); - return true; - } - $current[] = $user_id; - $new_list = array_values(array_unique(array_map('intval', $current))); - if ($this->forum->is_plate_cpt_active()) { - $result = update_post_meta($board_id, '_aksrt_moderators', $new_list); - } else { - $result = update_term_meta($board_id, '_aksrt_moderators', $new_list); - } - if ($result !== false) { - $this->set_moderator_role($user_id, $board_id, $role); - } - return $result !== false; - } - - /** - * Remove a moderator from a board. - */ - public function remove_moderator(int $user_id, int $board_id): bool { - $current = $this->get_board_moderators($board_id); - $current = array_values(array_diff($current, [$user_id])); - $roles = $this->get_moderator_roles($board_id); - unset($roles[$user_id]); - if ($this->forum->is_plate_cpt_active()) { - $result = update_post_meta($board_id, '_aksrt_moderators', $current); - update_post_meta($board_id, '_aksrt_moderator_roles', $roles); - } else { - $result = update_term_meta($board_id, '_aksrt_moderators', $current); - update_term_meta($board_id, '_aksrt_moderator_roles', $roles); - } - return $result !== false; - } - - /** - * Get all boards a user moderates (with roles). - */ - public function get_user_moderator_boards(int $user_id): array { - $boards = []; - foreach ($this->forum->get_boards() as $board) { - $board_id = (int) ($board['id'] ?? 0); - if ($board_id <= 0) continue; - $moderators = $this->get_board_moderators($board_id); - if (in_array($user_id, $moderators, true)) { - $roles = $this->get_moderator_roles($board_id); - $boards[] = [ - 'board_id' => $board_id, - 'board_name' => (string) ($board['name'] ?? ''), - 'role' => $roles[$user_id] ?? 'moderator', - ]; - } - } - return $boards; - } - - /** - * Check if a user has a specific minimum tier on a board/plate. - * - * Tier hierarchy (aligned with zibll 4-level): - * forum_admin(4) > cat_moderator(3) > plate_author(2) > moderator(1) - * - * @param string $min_role 'moderator'|'plate_author'|'cat_moderator'|'forum_admin' - */ - public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool { - $tier_map = [ - 'junior_moderator' => Forum::TIER_MODERATOR, // backward-compat alias - 'moderator' => Forum::TIER_MODERATOR, - 'plate_author' => Forum::TIER_PLATE_AUTHOR, - 'super_moderator' => Forum::TIER_PLATE_AUTHOR, // backward-compat alias - 'cat_moderator' => Forum::TIER_CAT_MOD, - 'forum_admin' => Forum::TIER_FORUM_ADMIN, - ]; - $required = $tier_map[$min_role] ?? Forum::TIER_MODERATOR; - return $this->forum->get_user_plate_tier($user_id, $board_id) >= $required; - } - - /** - * Get board moderator applications (统一入口,自动检测 CPT/taxonomy). - */ - public function get_board_moderator_applications(int $board_id): array { - if ($this->forum->is_plate_cpt_active()) { - return $this->get_plate_moderator_applications($board_id); - } - - // Fallback: taxonomy - $applications = get_term_meta($board_id, '_aksrt_moderator_applications', true); - if (!is_array($applications)) { - return []; - } - - return array_values(array_filter(array_map(function ($application) use ($board_id): array { - if (!is_array($application)) { - $application = []; - } - - return [ - 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), - 'board_id' => $board_id, - 'user_id' => (int) ($application['user_id'] ?? 0), - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), - ]; - }, $applications))); - } - - /** - * Get plate moderator applications from CPT meta. - */ - public function get_plate_moderator_applications(int $plate_id): array { - if (!post_type_exists('aksrt_forum_plate')) { - return []; - } - - $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); - if (!is_array($applications)) { - return []; - } - - return array_values(array_filter(array_map(function ($application) use ($plate_id): array { - if (!is_array($application)) { - $application = []; - } - - return [ - 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), - 'board_id' => $plate_id, - 'user_id' => (int) ($application['user_id'] ?? 0), - 'reason' => (string) ($application['reason'] ?? ''), - 'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING), - 'created_at' => (string) ($application['created_at'] ?? ''), - 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), - 'review_note' => (string) ($application['review_note'] ?? ''), - 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), - ]; - }, $applications))); - } - - /** - * Get current user's board application. - */ - public function get_board_application_for_user(int $user_id, int $board_id): ?array { - $applications = $this->get_board_moderator_applications($board_id); - $matched = array_values(array_filter($applications, static function ($application) use ($user_id): bool { - return (int) ($application['user_id'] ?? 0) === $user_id; - })); - - if (empty($matched)) { - return null; - } - - usort($matched, static function ($left, $right): int { - return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); - }); - - $application = $matched[0]; - $application['status_label'] = $this->get_application_status_label((string) ($application['status'] ?? '')); - - return $application; - } - - public function find_application_by_id(string $application_id): ?array { - foreach ($this->get_moderator_application_list() as $application) { - if ((string) ($application['id'] ?? '') === $application_id) { - return $application; - } - } - - return null; - } - - public function get_application_status_label(string $status): string { - $map = [ - Forum::APPLICATION_PENDING => __('待处理', 'aksrt'), - Forum::APPLICATION_APPROVED => __('已通过', 'aksrt'), - Forum::APPLICATION_REJECTED => __('已拒绝', 'aksrt'), - ]; - - return $map[$status] ?? $map[Forum::APPLICATION_PENDING]; - } - - public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void { - if (!$this->forum->is_plate_cpt_active()) { - return; - } - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $this->sync_plate_counts($plate_id); - } - } - - public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void { - if (!$this->forum->is_plate_cpt_active()) { - return; - } - $post_id = (int) ($commentdata['comment_post_ID'] ?? 0); - if ($post_id <= 0) { - return; - } - $post = get_post($post_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $this->sync_plate_counts($plate_id); - } - } - - public function is_moderator(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return false; - } - - if ($this->is_forum_admin($user_id)) { - return true; - } - - // CPT mode: resolve plate from topic meta - if ($this->forum->is_plate_cpt_active()) { - $plate_id = $this->forum->get_topic_plate_id($topic_id); - if ($plate_id > 0) { - return $this->forum->get_user_plate_tier($user_id, $plate_id) >= self::TIER_MODERATOR; - } - } - - // Fallback: taxonomy-based boards - $boards = get_the_terms($topic_id, 'aksrt_forum_board'); - if ($boards && !is_wp_error($boards)) { - foreach ($boards as $board) { - if ($this->is_board_moderator($user_id, (int) $board->term_id)) { - return true; - } - } - } - - return false; - } -} diff --git a/src/AKSRT/Forum/PermissionService.php b/src/AKSRT/Forum/PermissionService.php deleted file mode 100644 index 2b5d65db..00000000 --- a/src/AKSRT/Forum/PermissionService.php +++ /dev/null @@ -1,420 +0,0 @@ - cap_key => bool */ - private static array $cap_cache = []; - /** @var array "uid:plate_id" => tier */ - private static array $tier_cache = []; - - /** @var Forum */ - private $forum; - - public function __construct($forum) { - $this->forum = $forum; - } - - // ── Capability matrix ───────────────────────────────────────── - - /** - * Capability definitions matrix. - * - * Keys: - * 'min_tier' — minimum TIER_* constant required - * 'context' — 'topic'|'plate'|'global'|'any' - * 'self' — true = only applies to own content - * 'logged' — shorthand: any logged-in user - * 'settings_key' — optional forum settings permissions key - */ - private function cap_defs(): array { - return [ - // ── Topic viewing ───────────────────────────────────────────── - 'topic_view_pending' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_view_hidden' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_view_deleted' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - - // ── Topic editing ───────────────────────────────────────────── - 'topic_edit' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_delete' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - - // ── Topic governance ────────────────────────────────────────── - 'topic_sticky_plate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_sticky_super' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], - 'topic_sticky_global' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - 'topic_featured' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_lock' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'topic_lock_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_highlight' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_score_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_move' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'topic_moderate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - - // ── Topic interaction (logged users) ────────────────────────── - 'topic_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_favorite' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'topic_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Reply governance ────────────────────────────────────────── - 'reply_adopt' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'], - 'reply_adopt_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'reply_top' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - // ── Reply interaction ───────────────────────────────────────── - 'reply_like' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'reply_score' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Plate management ────────────────────────────────────────── - 'plate_follow' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'plate_create' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - 'plate_manage' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'], - 'plate_delete' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'], - - // ── Moderator system ────────────────────────────────────────── - 'moderator_manage' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], - 'moderator_apply' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'moderator_review' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'], - - // ── User governance ─────────────────────────────────────────── - 'user_ban' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'global'], - 'user_report' => ['min_tier' => self::TIER_NONE, 'logged' => true], - - // ── Compose capabilities ────────────────────────────────────── - 'compose_post' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => ''], - 'compose_post_no_review' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'], - 'compose_upload_image' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], - 'compose_upload_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_video'], - 'compose_upload_file' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_file'], - 'compose_iframe_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_iframe_video'], - 'compose_set_type' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_visibility' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_cover' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'], - 'compose_set_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true], - 'compose_set_paid' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], - 'compose_set_hide' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'], - ]; - } - - // ── Tier resolution ─────────────────────────────────────────── - - /** - * Return the user's highest tier for a given plate. - * - * Result is cached per request on "$user_id:$plate_id". - */ - public function get_user_plate_tier(int $user_id, int $plate_id): int { - if ($user_id <= 0) { - return self::TIER_NONE; - } - - $cache_key = "$user_id:$plate_id"; - if (isset(self::$tier_cache[$cache_key])) { - return self::$tier_cache[$cache_key]; - } - - $tier = $this->compute_user_plate_tier($user_id, $plate_id); - self::$tier_cache[$cache_key] = $tier; - return $tier; - } - - private function compute_user_plate_tier(int $user_id, int $plate_id): int { - // Tier 4: WP admin or bbs_admin_users list - if ($this->forum->is_forum_admin($user_id)) { - return self::TIER_FORUM_ADMIN; - } - - // Tier 3: category moderator (plate_cat term meta 'moderator') - $cats = get_the_terms($plate_id, 'aksrt_plate_cat'); - if ($cats && !is_wp_error($cats)) { - foreach ($cats as $cat) { - if ($this->forum->is_cat_moderator($user_id, (int) $cat->term_id)) { - return self::TIER_CAT_MOD; - } - } - } - - // Tier 2: plate_author (plate CPT post_author) - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($plate_id); - if ($post && (int) $post->post_author === $user_id) { - return self::TIER_PLATE_AUTHOR; - } - - // Tier 1: in plate _aksrt_moderators list - $mods = get_post_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - return self::TIER_MODERATOR; - } - - return self::TIER_NONE; - } - // Fallback: taxonomy board - $roles = get_term_meta($plate_id, '_aksrt_moderator_roles', true); - $roles = is_array($roles) ? $roles : []; - $role = $roles[$user_id] ?? ''; - if ($role === 'super_moderator' || $role === 'plate_author') { - return self::TIER_PLATE_AUTHOR; - } - $mods = get_term_meta($plate_id, '_aksrt_moderators', true); - if (is_array($mods) && in_array($user_id, $mods, true)) { - return self::TIER_MODERATOR; - } - - return self::TIER_NONE; - } - - // ── Capability check ────────────────────────────────────────── - - /** - * Unified capability resolver. - * - * @param int $user_id User to check (0 = guest). - * @param string $cap Capability id from cap_defs(). - * @param int $context_id topic_id for 'topic' context, plate_id for 'plate' context. - * @return bool - */ - public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool { - $cache_key = "$user_id:$cap:$context_id"; - if (array_key_exists($cache_key, self::$cap_cache)) { - return self::$cap_cache[$cache_key]; - } - - $result = $this->compute_forum_cap($user_id, $cap, $context_id); - self::$cap_cache[$cache_key] = $result; - return $result; - } - - private function compute_forum_cap(int $user_id, string $cap, int $context_id): bool { - $defs = $this->cap_defs(); - if (!isset($defs[$cap])) { - return false; - } - $def = $defs[$cap]; - - $logged_only = !empty($def['logged']) && ($def['min_tier'] ?? self::TIER_NONE) === self::TIER_NONE; - $self_only = !empty($def['self']); - $context = $def['context'] ?? 'any'; - $min_tier = (int) ($def['min_tier'] ?? self::TIER_NONE); - $settings_key = $def['settings_key'] ?? ''; - // Guest short-circuit - if ($user_id <= 0) { - return false; - } - - // Forum-wide admin bypasses everything - if ($this->forum->is_forum_admin($user_id)) { - // Honour settings-gated compose caps even for admins when the feature is globally off - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (isset($perms[$settings_key]) && $perms[$settings_key] === false) { - return false; - } - } - return true; - } - - // 'logged' / any-user caps: just need to be logged in - if ($logged_only || ($min_tier === self::TIER_NONE && $context === 'any')) { - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { - return false; - } - } - return true; - } - - // 'self' caps: caller passes topic_id as context; check authorship - if ($self_only) { - if ($context_id <= 0) { - return false; - } - $post = get_post($context_id); - return $post && (int) $post->post_author === $user_id; - } - - // Global caps (no specific plate/topic context) - if ($context === 'global') { - if ($min_tier <= self::TIER_CAT_MOD) { - return $this->is_cat_moderator_anywhere($user_id); - } - return false; - } - - // Plate-context caps - if ($context === 'plate') { - $plate_id = $this->resolve_plate_id($context_id, $cap); - if ($plate_id <= 0) { - return false; - } - $tier = $this->get_user_plate_tier($user_id, $plate_id); - - // Check optional settings gate - if ($settings_key !== '') { - $perms = $this->forum->get_config()['permissions'] ?? []; - if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) { - return $tier >= self::TIER_PLATE_AUTHOR; - } - } - - return $tier >= $min_tier; - } - - return false; - } - - // ── Helper methods ──────────────────────────────────────────── - - /** - * Resolve a plate_id from either a plate_id or topic_id, depending on the cap context. - */ - private function resolve_plate_id(int $context_id, string $cap): int { - if ($context_id <= 0) { - return 0; - } - $post = get_post($context_id); - if (!$post) { - return 0; - } - if ($post->post_type === 'aksrt_forum_plate') { - return $context_id; - } - if ($post->post_type === 'aksrt_forum_topic') { - return $this->forum->get_topic_plate_id($context_id); - } - return 0; - } - - /** - * Check if the user is a cat_moderator on ANY plate category (for global caps). - */ - private function is_cat_moderator_anywhere(int $user_id): bool { - if ($user_id <= 0) { - return false; - } - if ($this->forum->is_forum_admin($user_id)) { - return true; - } - $cats = get_terms(['taxonomy' => 'aksrt_plate_cat', 'hide_empty' => false, 'fields' => 'ids']); - if (is_wp_error($cats) || empty($cats)) { - return false; - } - foreach ((array) $cats as $cat_id) { - if ($this->forum->is_cat_moderator($user_id, (int) $cat_id)) { - return true; - } - } - return false; - } - - // ── Compose capabilities ────────────────────────────────────── - - /** - * Get per-feature capabilities for the compose/edit form. - * - * @return array - */ - public function get_compose_capabilities(int $user_id): array { - $keys = [ - 'can_post', 'can_set_type', 'can_set_visibility', - 'can_set_cover', 'can_set_cover_image', 'can_set_cover_slide', 'can_set_cover_video', - 'can_set_vote', 'can_set_paid', 'can_set_hide', - 'can_upload_image', 'can_upload_video', 'can_upload_file', - 'can_iframe_video', - ]; - if ($user_id <= 0) { - return array_fill_keys($keys, false); - } - - return [ - 'can_post' => $this->forum_user_can($user_id, 'compose_post'), - 'can_set_type' => $this->forum_user_can($user_id, 'compose_set_type'), - 'can_set_visibility' => $this->forum_user_can($user_id, 'compose_set_visibility'), - 'can_set_cover' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_image' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_slide' => $this->forum_user_can($user_id, 'compose_set_cover'), - 'can_set_cover_video' => $this->forum_user_can($user_id, 'compose_upload_video'), - 'can_set_vote' => $this->forum_user_can($user_id, 'compose_set_vote'), - 'can_set_paid' => $this->forum_user_can($user_id, 'compose_set_paid'), - 'can_set_hide' => $this->forum_user_can($user_id, 'compose_set_hide'), - 'can_upload_image' => $this->forum_user_can($user_id, 'compose_upload_image'), - 'can_upload_video' => $this->forum_user_can($user_id, 'compose_upload_video'), - 'can_upload_file' => $this->forum_user_can($user_id, 'compose_upload_file'), - 'can_iframe_video' => $this->forum_user_can($user_id, 'compose_iframe_video'), - ]; - } - - // ── Cache invalidation ──────────────────────────────────────── - - public function flush_cap_cache(int $user_id = 0): void { - if ($user_id > 0) { - foreach (array_keys(self::$cap_cache) as $k) { - if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { - unset(self::$cap_cache[$k]); - } - } - foreach (array_keys(self::$tier_cache) as $k) { - if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) { - unset(self::$tier_cache[$k]); - } - } - } else { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - public function cap_cache_on_login(string $user_login, \WP_User $user): void { - $this->flush_cap_cache((int) $user->ID); - } - - /** Flush cache when plate moderator meta changes. */ - public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void { - // deleted_post_meta passes $meta_ids as array; other hooks pass int meta_id. - if (is_array($meta_key) || (is_array($meta_id) && !is_string($meta_key))) { - // Still accept call; $meta_key should be string on WP hooks. - } - $meta_key = is_string($meta_key) ? $meta_key : ''; - if (in_array($meta_key, ['_aksrt_moderators', '_aksrt_moderator_roles'], true)) { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - /** Flush cache when plate_cat moderator term meta changes. */ - public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void { - $meta_key = is_string($meta_key) ? $meta_key : ''; - if ($meta_key === 'moderator') { - self::$cap_cache = []; - self::$tier_cache = []; - } - } - - /** Flush cache when a plate CPT post is saved (plate_author may change). */ - public function cap_cache_on_plate_save($post_id): void { - $post_id = (int) $post_id; - foreach (array_keys(self::$tier_cache) as $k) { - if (substr($k, -strlen(":$post_id")) === ":$post_id") { - unset(self::$tier_cache[$k]); - } - } - self::$cap_cache = []; - } -} diff --git a/src/AKSRT/Forum/PlateAdminList.php b/src/AKSRT/Forum/PlateAdminList.php deleted file mode 100644 index 478f688f..00000000 --- a/src/AKSRT/Forum/PlateAdminList.php +++ /dev/null @@ -1,107 +0,0 @@ - $label) { - $new[$key] = $label; - if ($key === 'title') { - $new['aksrt_plate_mods'] = __('版主数', 'aksrt'); - $new['aksrt_plate_posts'] = __('帖子数', 'aksrt'); - $new['aksrt_plate_reply'] = __('回复数', 'aksrt'); - $new['aksrt_plate_views'] = __('浏览', 'aksrt'); - $new['aksrt_plate_follow'] = __('关注', 'aksrt'); - $new['aksrt_plate_pending_apps'] = __('待审申请', 'aksrt'); - } - } - return $new; - } - - public function sortable_columns(array $columns): array { - $columns['aksrt_plate_posts'] = 'aksrt_posts_count'; - $columns['aksrt_plate_views'] = 'aksrt_plate_views'; - return $columns; - } - - public function render_column(string $column, int $post_id): void { - switch ($column) { - case 'aksrt_plate_mods': - $mods = get_post_meta($post_id, '_aksrt_moderators', true); - $count = is_array($mods) ? count(array_filter(array_map('intval', $mods))) : 0; - echo esc_html((string) $count); - break; - case 'aksrt_plate_posts': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_posts_count', true)); - break; - case 'aksrt_plate_reply': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_reply_count', true)); - break; - case 'aksrt_plate_views': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_plate_views', true)); - break; - case 'aksrt_plate_follow': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_follow_count', true)); - break; - case 'aksrt_plate_pending_apps': - $apps = get_post_meta($post_id, '_aksrt_moderator_applications', true); - $pending = 0; - if (is_array($apps)) { - foreach ($apps as $app) { - if (is_array($app) && (($app['status'] ?? '') === 'pending')) { - $pending++; - } - } - } - if ($pending > 0) { - echo '' . esc_html((string) $pending) . ''; - } else { - echo '0'; - } - break; - } - } - - public function apply_orderby(\WP_Query $query): void { - if (!is_admin() || !$query->is_main_query()) { - return; - } - if ($query->get('post_type') !== 'aksrt_forum_plate') { - return; - } - $orderby = $query->get('orderby'); - if ($orderby === 'aksrt_posts_count') { - $query->set('meta_key', '_aksrt_posts_count'); - $query->set('orderby', 'meta_value_num'); - } elseif ($orderby === 'aksrt_plate_views') { - $query->set('meta_key', '_aksrt_plate_views'); - $query->set('orderby', 'meta_value_num'); - } - } -} diff --git a/src/AKSRT/Forum/PlateFollowPay.php b/src/AKSRT/Forum/PlateFollowPay.php deleted file mode 100644 index ef50b956..00000000 --- a/src/AKSRT/Forum/PlateFollowPay.php +++ /dev/null @@ -1,852 +0,0 @@ -\d+)', [ - [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_preview'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - [ - 'methods' => 'POST', - 'callback' => [$this, 'rest_purchase'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - ]); - } - - /** - * @return true|\WP_Error - */ - 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_preview(\WP_REST_Request $request) { - $plate_id = (int) $request['id']; - $uid = get_current_user_id(); - $coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? '')); - $preview = $this->preview($plate_id, $uid, $coupon_code); - if (is_wp_error($preview)) { - return $preview; - } - return new \WP_REST_Response($preview, 200); - } - - public function rest_purchase(\WP_REST_Request $request) { - $plate_id = (int) $request['id']; - $uid = get_current_user_id(); - $post = get_post($plate_id); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($uid <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), - ]); - } - - $params = $request->get_json_params(); - if (!is_array($params)) { - $params = $request->get_params(); - } - - $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); - if ($payment_method === '' || $payment_method === 'cash') { - $payment_method = 'balance'; - } - if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - $coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? '')); - - return $this->rest_wrap_result( - $this->create_cash_order($plate_id, $uid, $payment_method, $coupon_code) - ); - } - - /** - * @param mixed $result - * @return \WP_REST_Response|\WP_Error - */ - private function rest_wrap_result($result) { - if (is_wp_error($result)) { - return $result; - } - if (empty($result['success'])) { - $code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed')); - $status = (int) ($result['status'] ?? 400); - return new \WP_Error( - $code !== '' ? $code : 'aksrt_pay_failed', - (string) ($result['message'] ?? __('支付失败', 'aksrt')), - array_merge( - ['status' => $status > 0 ? $status : 400], - is_array($result['data'] ?? null) ? $result['data'] : [] - ) - ); - } - return new \WP_REST_Response($result, 200); - } - - /** - * @return array|\WP_Error - */ - public function preview(int $plate_id, int $user_id = 0, string $coupon_code = '') { - $assert = $this->assert_plate($plate_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - $uid = $user_id > 0 ? $user_id : get_current_user_id(); - $coupon_code = sanitize_text_field($coupon_code); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - - if ($uid <= 0 && !$allow_guest) { - return [ - 'post_id' => $plate_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'plate_follow', - 'pay_type_label' => __('付费关注', 'aksrt'), - 'amount' => 0, - 'base' => 0, - 'list_amount' => 0, - 'payable' => 0, - 'currency' => '¥', - 'amount_label' => '', - 'base_label' => '', - 'list_amount_label' => '', - 'is_free' => false, - 'has_paid_access' => false, - 'need_login' => true, - 'allow_guest' => false, - 'need_vip' => false, - 'need_purchase' => true, - 'logged_in' => false, - 'balance' => 0, - 'balance_enough' => false, - 'payment_methods' => [], - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => false, - 'coupon_desc' => '', - 'coupon_code' => '', - 'coupon' => null, - 'coupon_error' => '', - 'coupon_discount' => 0, - 'coupon_discount_label' => '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - ]; - } - - $priced = $this->resolve_price($plate_id, $uid); - if (($priced['modo'] ?? 'money') === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($uid <= 0) { - $vip_free = false; - } - $has_followed = $this->user_has_followed($uid, $plate_id); - - $wallet_balance = 0.0; - if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) { - $wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid); - } - - $methods = function_exists('aksrt_list_cash_payment_methods') - ? aksrt_list_cash_payment_methods($uid > 0) - : []; - - $coupon_allowed = $this->is_coupon_allowed($plate_id); - $coupon_error = ''; - $coupon_discount = 0.0; - $coupon_meta = null; - $applied_code = ''; - $payable = $list_amount; - - if (!$vip_free && $list_amount > 0) { - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, $coupon_code, true); - if (is_wp_error($discounted)) { - $coupon_error = $discounted->get_error_message(); - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, '', true); - } - if (!is_wp_error($discounted)) { - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $payable = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null; - $applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? '')); - if ($coupon_error === '' && !empty($discounted['coupon_error'])) { - $coupon_error = (string) $discounted['coupon_error']; - } - } - } elseif ($vip_free) { - $payable = 0.0; - $list_amount = 0.0; - } - - $is_free = $vip_free || $payable <= 0; - $amount_label = $is_free - ? __('免费', 'aksrt') - : sprintf('¥%s', number_format($payable, 2, '.', '')); - $list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', '')); - $base_label = sprintf('¥%s', number_format($base, 2, '.', '')); - - return [ - 'post_id' => $plate_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'plate_follow', - 'pay_type_label' => __('付费关注', 'aksrt'), - 'amount' => $payable, - 'base' => $base, - 'list_amount' => $list_amount > 0 ? $list_amount : $base, - 'list_amount_label' => $list_label, - 'payable' => $payable, - 'currency' => '¥', - 'amount_label' => $amount_label, - 'base_label' => $base_label, - 'is_free' => $is_free, - 'has_paid_access' => $has_followed, - 'paid_order' => null, - 'pending_order' => null, - 'need_login' => $uid <= 0 && !$allow_guest, - 'allow_guest' => $allow_guest, - 'need_vip' => false, - 'need_purchase' => !$has_followed && !$is_free, - 'logged_in' => $uid > 0, - 'balance' => $wallet_balance, - 'wallet_balance' => $wallet_balance, - 'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable), - 'payment_methods' => $methods, - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => $coupon_allowed, - 'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '', - 'coupon_code' => $applied_code, - 'coupon' => $coupon_meta, - 'coupon_error' => $coupon_error, - 'coupon_discount' => $coupon_discount, - 'coupon_discount_label' => $coupon_discount > 0 - ? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', ''))) - : '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'is_vip_price' => !empty($priced['is_vip_price']), - ]; - } - - /** - * @return array|\WP_Error - */ - public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') { - $assert = $this->assert_plate($plate_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($user_id <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url(get_permalink($post)), - ]); - } - - if ($this->user_has_followed($user_id, $plate_id)) { - return new \WP_Error('aksrt_already_followed', __('已关注该版块。', 'aksrt'), [ - 'status' => 400, - 'reload' => true, - ]); - } - - $priced = $this->resolve_price($plate_id, $user_id); - if (($priced['modo'] ?? 'money') === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($user_id <= 0) { - $vip_free = false; - } - $coupon_code = sanitize_text_field($coupon_code); - - // VIP free — no coupon stack. Guests never hit this. - if ($vip_free) { - return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [ - 'list_amount' => 0.0, - 'payable' => 0.0, - 'coupon_code' => '', - 'coupon_discount' => 0.0, - 'coupon' => [], - 'message' => __('VIP 免费关注成功', 'aksrt'), - 'paid_note' => __('VIP 免费关注版块。', 'aksrt'), - ]); - } - - if ($list_amount <= 0) { - return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]); - } - - $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $user_id, $coupon_code, false); - if (is_wp_error($discounted)) { - return $discounted; - } - - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $amount = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : []; - $coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code); - - // Free after coupon. - if ($amount <= 0) { - return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [ - 'list_amount' => $list_amount, - 'payable' => 0.0, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'message' => __('无需支付,已关注。', 'aksrt'), - 'paid_note' => __('优惠后无需支付。', 'aksrt'), - ]); - } - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]); - } - - $payment_method = sanitize_key($payment_method); - if ($payment_method === '') { - $payment_method = $user_id > 0 ? 'balance' : 'alipay'; - } - if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - - $pending = $this->find_pending_order($user_id, $plate_id); - $can_resume = false; - if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) { - $pending_meta = $this->decode_order_note_meta($pending); - $pending_coupon = (string) ($pending_meta['coupon_code'] ?? ''); - if (is_array($pending_meta['coupon'] ?? null)) { - $pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon); - } - if ($pending_coupon === $coupon_code_norm) { - $can_resume = true; - } - } - - if ($can_resume) { - $order_number = (string) ($pending['order_number'] ?? ''); - $order = $pending; - } else { - $note = wp_json_encode([ - 'plate_follow_pay' => 1, - 'plate_id' => $plate_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => $base, - 'list_amount' => $list_amount, - 'amount' => $amount, - 'payable' => $amount, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'product_id' => $plate_id, - 'amount' => $amount, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - } - - $order['subject'] = sprintf( - /* translators: %s: plate title */ - __('付费关注版块「%s」', 'aksrt'), - get_the_title($post) - ); - $order['payment_method'] = $payment_method; - $order['amount'] = $amount; - - if (!class_exists('\AKSRT\Payment\Gateway')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]); - } - - $resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method); - if (empty($resolved['gateway'])) { - return new \WP_Error( - (string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'), - (string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')), - ['status' => 400, 'order_number' => $order_number] - ); - } - - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [ - 'paid_note' => __('用户完成版块付费关注支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return new \WP_Error( - (string) ($payment_result['code'] ?? 'aksrt_pay_failed'), - (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')), - ['status' => 402, 'order_number' => $order_number] - ); - } - - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => false, - 'poll' => true, - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => true, - ], - ]; - } - - public function on_order_paid(string $order_number, array $paid_order): void { - $product_type = sanitize_key((string) ($paid_order['product_type'] ?? '')); - if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW) { - return; - } - - $plate_id = (int) ($paid_order['product_id'] ?? 0); - $user_id = (int) ($paid_order['user_id'] ?? 0); - if ($plate_id <= 0) { - return; - } - - $meta = $this->decode_order_note_meta($paid_order); - if (empty($meta['plate_follow_pay'])) { - $post = get_post($plate_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') { - return; - } - } - - $fulfilled = $user_id <= 0; - if ($user_id > 0) { - $this->mark_followed($user_id, $plate_id); - $fulfilled = class_exists('AKSRT\\Forum\\Forum') - ? Forum::get_instance()->is_user_following_plate($user_id, $plate_id) - : in_array($plate_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_follow_plate', true)), true); - } elseif (function_exists('aksrt_set_guest_pay_cookies')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (is_array($full)) { - $paid_order = array_merge($full, $paid_order); - } - } - aksrt_set_guest_pay_cookies($paid_order); - } - $this->finalize_plate_order_side_effects($order_number, $paid_order); - if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('付费关注权益已发放。', 'aksrt')] - ); - } - } - - /** - * @return array{post:\WP_Post}|\WP_Error - */ - private function assert_plate(int $plate_id) { - $post = get_post($plate_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') { - return new \WP_Error('aksrt_invalid', __('版块不存在', 'aksrt'), ['status' => 404]); - } - if ($post->post_status !== 'publish') { - return new \WP_Error('aksrt_invalid', __('版块不可关注', 'aksrt'), ['status' => 404]); - } - - if (!$this->is_follow_pay_enabled($plate_id)) { - return new \WP_Error('aksrt_not_paid', __('该版块未开启付费关注', 'aksrt'), ['status' => 400]); - } - - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if ($modo === 'points') { - return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); - } - - return ['post' => $post]; - } - - private function is_follow_pay_enabled(int $plate_id): bool { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_plate_follow_pay_enabled')) { - return (bool) Forum::get_instance()->is_plate_follow_pay_enabled($plate_id); - } - return (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; - } - - /** - * @return array{amount:float,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float,vip_level:int} - */ - private function resolve_price(int $plate_id, int $user_id): array { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'resolve_plate_follow_pay_price')) { - return Forum::get_instance()->resolve_plate_follow_pay_price($plate_id, $user_id); - } - return [ - 'amount' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), - 'modo' => 'money', - 'is_vip_price' => false, - 'is_free' => false, - 'label' => '', - 'base' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), - 'vip_level' => 0, - ]; - } - - private function user_has_followed(int $user_id, int $plate_id): bool { - if ($plate_id <= 0) { - return false; - } - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_plate_follow_access') - && aksrt_guest_has_plate_follow_access($plate_id); - } - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_user_following_plate')) { - return (bool) Forum::get_instance()->is_user_following_plate($user_id, $plate_id); - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - return false; - } - return in_array($plate_id, array_map('intval', $follow_list), true); - } - - private function mark_followed(int $user_id, int $plate_id): void { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_plate_followed')) { - Forum::get_instance()->mark_plate_followed($user_id, $plate_id); - return; - } - if ($user_id <= 0 || $plate_id <= 0) { - return; - } - $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); - if (!is_array($follow_list)) { - $follow_list = []; - } - $follow_list[] = $plate_id; - update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); - } - - /** - * Free VIP or free-after-discount paid order. - * - * @param array{ - * list_amount?:float, - * payable?:float, - * coupon_code?:string, - * coupon_discount?:float, - * coupon?:array, - * message?:string, - * paid_note?:string - * } $opts - * @return array|\WP_Error - */ - private function create_free_paid_order(int $plate_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) { - $list_amount = round((float) ($opts['list_amount'] ?? 0), 2); - $payable = round((float) ($opts['payable'] ?? 0), 2); - $coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? '')); - $coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : []; - $message = (string) ($opts['message'] ?? __('VIP 免费关注成功', 'aksrt')); - $paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费关注版块。', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - $this->mark_followed($user_id, $plate_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $note = wp_json_encode([ - 'plate_follow_pay' => 1, - 'plate_id' => $plate_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => (float) ($priced['base'] ?? 0), - 'list_amount' => $list_amount, - 'amount' => $payable, - 'payable' => $payable, - 'coupon_code' => $coupon_code, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'free' => 1, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'product_id' => $plate_id, - 'amount' => 0.0, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => 'free', - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - $this->mark_followed($user_id, $plate_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'free', - 'payment_transaction_id' => 'free_' . $order_number, - 'note' => $paid_note, - ]); - if (!$paid) { - return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); - } - $this->mark_followed($user_id, $plate_id); - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - private function is_coupon_allowed(int $plate_id): bool { - return (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1; - } - - /** - * VIP list price → coupon → payable. - * - * @param array $priced - * @return array|\WP_Error - */ - private function apply_plate_cash_discounts(array $priced, int $plate_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) { - $list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2); - $pay = $list_amount; - $coupon_discount = 0.0; - $coupon_meta = []; - $coupon_error = ''; - $coupon_code = sanitize_text_field($coupon_code); - - if ($coupon_code !== '') { - if (!$this->is_coupon_allowed($plate_id)) { - $err = new \WP_Error('aksrt_coupon_not_allowed', __('当前版块不支持优惠码。', 'aksrt'), ['status' => 400]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } elseif (!class_exists('\AKSRT\Payment\Coupon')) { - $err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [ - 'user_id' => max(0, $user_id), - 'product_ids' => [$plate_id], - 'context' => 'plate_follow', - ]); - if (empty($result['valid'])) { - $err = new \WP_Error( - 'aksrt_coupon_error', - (string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')), - ['status' => 400] - ); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2); - if ($coupon_discount > $pay) { - $coupon_discount = $pay; - } - $pay = round(max(0.0, $pay - $coupon_discount), 2); - $c = is_array($result['coupon'] ?? null) ? $result['coupon'] : []; - $coupon_meta = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? $coupon_code), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'discount' => $coupon_discount, - ]; - $coupon_code = (string) $coupon_meta['code']; - } - } - } - - return [ - 'list_amount' => $list_amount, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'coupon_code' => $coupon_code, - 'coupon_error' => $coupon_error, - 'payable' => round(max(0.0, $pay), 2), - ]; - } - - private function finalize_plate_order_side_effects(string $order_number, array $order): void { - $meta = $this->decode_order_note_meta($order); - if (empty($meta['plate_follow_pay'])) { - return; - } - if (($meta['pay_modo'] ?? '') === 'points') { - return; - } - $buyer_id = (int) ($order['user_id'] ?? 0); - if (function_exists('aksrt_burn_order_coupon')) { - aksrt_burn_order_coupon($order_number, $meta, $buyer_id); - } - } - - private function find_pending_order(int $user_id, int $plate_id): ?array { - if ($user_id <= 0 || $plate_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) { - return null; - } - $orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [ - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW, - 'status' => \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING, - 'limit' => 20, - 'offset' => 0, - ]); - foreach ($orders as $order) { - if ((int) ($order['product_id'] ?? 0) !== $plate_id) { - continue; - } - $meta = $this->decode_order_note_meta($order); - if (!empty($meta['plate_follow_pay'])) { - return $order; - } - return $order; - } - return null; - } - - private function decode_order_note_meta(array $order): array { - if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) { - return $order['meta_payload']; - } - $note = (string) ($order['note'] ?? ''); - if ($note === '') { - return []; - } - $decoded = json_decode($note, true); - return is_array($decoded) ? $decoded : []; - } -} diff --git a/src/AKSRT/Forum/PlateMetaBox.php b/src/AKSRT/Forum/PlateMetaBox.php deleted file mode 100644 index b673ca57..00000000 --- a/src/AKSRT/Forum/PlateMetaBox.php +++ /dev/null @@ -1,581 +0,0 @@ - */ - private const ASSIGNABLE_ROLES = [ - 'super_moderator' => '超级版主', - 'moderator' => '版主', - 'junior_moderator' => '初级版主', - ]; - - /** @var list */ - private const ALLOW_VIEW = ['public', 'login', 'follow', 'role']; - - /** @var list */ - private const ADD_LIMIT = ['all', 'login', 'moderator', 'role']; - - public static function get_instance(): self { - if (null === self::$instance) { - self::$instance = new self(); - } - return self::$instance; - } - - public function init(): void { - if (!is_admin()) { - return; - } - add_action('add_meta_boxes', [$this, 'register_meta_box']); - add_action('save_post_aksrt_forum_plate', [$this, 'save'], 20, 2); - } - - public function register_meta_box(): void { - add_meta_box( - 'aksrt_plate_moderators', - __('版主与权限', 'aksrt'), - [$this, 'render'], - 'aksrt_forum_plate', - 'side', - 'high' - ); - } - - public function render(\WP_Post $post): void { - wp_nonce_field('aksrt_plate_meta_save', 'aksrt_plate_meta_nonce'); - - $plate_id = (int) $post->ID; - $author_id = (int) $post->post_author; - $moderator_ids = []; - $roles_map = []; - $applications = []; - - if (class_exists('AKSRT\\Forum\\Forum')) { - $forum = Forum::get_instance(); - $moderator_ids = $forum->get_plate_moderators($plate_id); - $roles_map = $forum->get_moderator_roles($plate_id); - $applications = $forum->get_plate_moderator_applications($plate_id); - } else { - $raw = get_post_meta($plate_id, '_aksrt_moderators', true); - $moderator_ids = is_array($raw) ? array_map('intval', $raw) : []; - $roles_raw = get_post_meta($plate_id, '_aksrt_moderator_roles', true); - $roles_map = is_array($roles_raw) ? $roles_raw : []; - $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); - $applications = is_array($applications) ? $applications : []; - } - - $allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'); - if (!in_array($allow_view, self::ALLOW_VIEW, true)) { - // Rewrite legacy values (e.g. moderator) on display; save will persist new contract. - $allow_view = 'login'; - } - $add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'); - if (!in_array($add_limit, self::ADD_LIMIT, true)) { - $add_limit = 'all'; - } - $view_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true); - $view_roles = is_array($view_roles) ? array_map('sanitize_key', $view_roles) : []; - $add_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); - $add_roles = is_array($add_roles) ? array_map('sanitize_key', $add_roles) : []; - $wp_roles = $this->wp_role_options(); - $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); - $sort_order = (int) get_post_meta($plate_id, '_aksrt_sort_order', true); - $follow_pay = $this->get_follow_pay_meta($plate_id); - - $this->render_access_fields($allow_view, $add_limit, $view_roles, $add_roles, $wp_roles); - $this->render_follow_pay_fields($follow_pay); - $this->render_display_fields($icon, $sort_order); - $this->render_moderator_fields($moderator_ids, $roles_map, $author_id); - $this->render_applications($applications); - } - - /** - * @param list $view_roles - * @param list $add_roles - * @param array $wp_roles - */ - private function render_access_fields(string $allow_view, string $add_limit, array $view_roles, array $add_roles, array $wp_roles): void { - ?> -

    - - -

    -

    - - - -

    -

    - - -

    -

    - - - -

    -
    - -

    - - -

    -

    - - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - - -

    -

    - - -

    -
    - -

    - - - -

    -

    - - - -

    -
    - $moderator_ids - * @param array $roles_map - */ - private function render_moderator_fields(array $moderator_ids, array $roles_map, int $author_id): void { - if ($author_id > 0) { - $author = get_userdata($author_id); - echo '

    ' . esc_html__('超级版主(版块作者)', 'aksrt') . '
    '; - echo '' . esc_html($author ? $author->user_login . ' (#' . $author_id . ')' : (string) $author_id) . ''; - echo '
    ' . esc_html__('由版块作者决定,不可在此移除。', 'aksrt') . '

    '; - } - - echo '

    ' . esc_html__('版主列表', 'aksrt') . '

    '; - $rows = 0; - foreach ($moderator_ids as $uid) { - $uid = (int) $uid; - if ($uid <= 0 || $uid === $author_id) { - continue; - } - $user = get_userdata($uid); - $role = sanitize_key((string) ($roles_map[$uid] ?? 'moderator')); - if (!isset(self::ASSIGNABLE_ROLES[$role])) { - $role = 'moderator'; - } - $rows++; - echo '
    '; - echo '
    ' . esc_html($user ? $user->user_login . ' (#' . $uid . ')' : (string) $uid) . '
    '; - echo ''; - echo ''; - echo '
    '; - } - if ($rows === 0) { - echo '

    ' . esc_html__('暂无版主。', 'aksrt') . '

    '; - } - - echo '

    ' . esc_html__('添加版主', 'aksrt') . '

    '; - echo '

    '; - echo '

    '; - echo '

    ' . esc_html__('保存后生效。勿将版块作者重复加为版主。', 'aksrt') . '

    '; - echo '
    '; - } - - /** - * @param array> $applications - */ - private function render_applications(array $applications): void { - echo '

    ' . esc_html__('版主申请', 'aksrt') . '

    '; - $pending = array_values(array_filter($applications, static function ($app) { - if (!is_array($app)) { - return false; - } - $status = (string) ($app['status'] ?? ''); - if ($status === 'pending') { - return true; - } - return class_exists('AKSRT\\Forum\\Forum') && $status === Forum::APPLICATION_PENDING; - })); - if (!$pending) { - echo '

    ' . esc_html__('暂无待处理申请。', 'aksrt') . '

    '; - return; - } - - echo '
      '; - foreach ($pending as $app) { - $uid = (int) ($app['user_id'] ?? 0); - $user = $uid > 0 ? get_userdata($uid) : null; - $aid = (string) ($app['id'] ?? ''); - $reason = (string) ($app['reason'] ?? ''); - echo '
    • '; - echo '
      ' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : (string) $uid) . '
      '; - if ($reason !== '') { - echo '
      ' . esc_html($reason) . '
      '; - } - echo ''; - echo ''; - echo ''; - echo '
    • '; - } - echo '
    '; - } - - public function save(int $post_id, \WP_Post $post): void { - if (!isset($_POST['aksrt_plate_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_plate_meta_nonce'])), 'aksrt_plate_meta_save')) { - return; - } - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return; - } - if ($post->post_type !== 'aksrt_forum_plate' || !current_user_can('edit_post', $post_id)) { - return; - } - - $this->save_access_meta($post_id); - $this->save_follow_pay_meta($post_id); - $this->save_display_meta($post_id); - $this->save_moderators($post_id, (int) $post->post_author); - $this->save_applications(); - } - - private function save_display_meta(int $post_id): void { - $icon = isset($_POST['aksrt_plate_icon']) - ? sanitize_text_field((string) wp_unslash($_POST['aksrt_plate_icon'])) - : ''; - update_post_meta($post_id, '_aksrt_icon', $icon); - - $sort = isset($_POST['aksrt_plate_sort_order']) - ? (int) wp_unslash($_POST['aksrt_plate_sort_order']) - : 0; - update_post_meta($post_id, '_aksrt_sort_order', $sort); - } - - private function save_access_meta(int $post_id): void { - $allow_view = isset($_POST['aksrt_plate_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_allow_view'])) : 'public'; - if (!in_array($allow_view, self::ALLOW_VIEW, true)) { - $allow_view = 'public'; - } - update_post_meta($post_id, '_aksrt_allow_view', $allow_view); - - if ($allow_view === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_plate_allow_view_roles'] ?? []); - update_post_meta($post_id, '_aksrt_allow_view_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_allow_view_roles'); - } - - $add_limit = isset($_POST['aksrt_plate_add_limit']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_add_limit'])) : 'all'; - if (!in_array($add_limit, self::ADD_LIMIT, true)) { - $add_limit = 'all'; - } - update_post_meta($post_id, '_aksrt_add_limit', $add_limit); - - if ($add_limit === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_plate_add_limit_roles'] ?? []); - update_post_meta($post_id, '_aksrt_add_limit_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_add_limit_roles'); - } - } - - private function save_follow_pay_meta(int $post_id): void { - $enabled = !empty($_POST['aksrt_plate_follow_pay_enabled']); - $modo = isset($_POST['aksrt_plate_follow_pay_modo']) - ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_follow_pay_modo'])) - : 'money'; - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - $price = isset($_POST['aksrt_plate_follow_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_price'])) : 0; - $points = isset($_POST['aksrt_plate_follow_pay_points']) ? max(0, (int) wp_unslash($_POST['aksrt_plate_follow_pay_points'])) : 0; - - // Invalid price while enabled → force off (normalize, no migration). - if ($enabled) { - if ($modo === 'points' && $points <= 0) { - $enabled = false; - } - if ($modo === 'money' && $price <= 0) { - $enabled = false; - } - } - - if (!$enabled) { - delete_post_meta($post_id, '_aksrt_follow_pay'); - delete_post_meta($post_id, '_aksrt_follow_pay_modo'); - delete_post_meta($post_id, '_aksrt_follow_pay_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_points'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s'); - return; - } - - update_post_meta($post_id, '_aksrt_follow_pay', 1); - update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo); - update_post_meta($post_id, '_aksrt_follow_pay_price', $price); - update_post_meta($post_id, '_aksrt_follow_pay_points', $points); - update_post_meta($post_id, '_aksrt_follow_pay_coupon_s', !empty($_POST['aksrt_plate_follow_pay_coupon_s']) ? 1 : 0); - - if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') { - $vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price'])); - update_post_meta($post_id, '_aksrt_follow_pay_vip1_price', $vip1); - // Keep legacy single VIP key in sync with VIP1 for old readers. - update_post_meta($post_id, '_aksrt_follow_pay_vip_price', $vip1); - } else { - delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); - } - - if (isset($_POST['aksrt_plate_follow_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']) !== '') { - update_post_meta($post_id, '_aksrt_follow_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']))); - } else { - delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); - } - } - - /** - * @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string,coupon_s:bool} - */ - private function get_follow_pay_meta(int $plate_id): array { - $enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; - $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); - if (!in_array($modo, ['money', 'points'], true)) { - $modo = 'money'; - } - - $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true); - $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); - if (!$vip1_set) { - // Legacy single VIP key → vip1. - $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true); - $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); - } - - $vip2_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true); - $vip2_set = ($vip2_raw !== '' && $vip2_raw !== null && $vip2_raw !== false); - - return [ - 'enabled' => $enabled, - 'modo' => $modo, - 'price' => (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true), - 'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true), - 'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '', - 'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '', - 'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1, - ]; - } - - private function save_moderators(int $post_id, int $author_id): void { - $role_posted = isset($_POST['aksrt_plate_mod_role']) && is_array($_POST['aksrt_plate_mod_role']) - ? wp_unslash($_POST['aksrt_plate_mod_role']) - : []; - $remove = isset($_POST['aksrt_plate_mod_remove']) && is_array($_POST['aksrt_plate_mod_remove']) - ? array_map('intval', wp_unslash($_POST['aksrt_plate_mod_remove'])) - : []; - $remove_set = array_fill_keys($remove, true); - - $desired = []; - foreach ($role_posted as $uid => $role) { - $uid = (int) $uid; - $role = sanitize_key((string) $role); - if ($uid <= 0 || $uid === $author_id || isset($remove_set[$uid])) { - continue; - } - if (!isset(self::ASSIGNABLE_ROLES[$role])) { - $role = 'moderator'; - } - $desired[$uid] = $role; - } - - $add_id = isset($_POST['aksrt_plate_mod_add_id']) ? (int) wp_unslash($_POST['aksrt_plate_mod_add_id']) : 0; - $add_role = isset($_POST['aksrt_plate_mod_add_role']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_mod_add_role'])) : 'moderator'; - if ($add_id > 0 && $add_id !== $author_id) { - if (!isset(self::ASSIGNABLE_ROLES[$add_role])) { - $add_role = 'moderator'; - } - $desired[$add_id] = $add_role; - } - - if (class_exists('AKSRT\\Forum\\Forum')) { - $forum = Forum::get_instance(); - $current = $forum->get_plate_moderators($post_id); - foreach ($current as $uid) { - $uid = (int) $uid; - if ($uid === $author_id) { - continue; - } - if (!isset($desired[$uid])) { - $forum->remove_moderator($uid, $post_id); - } - } - foreach ($desired as $uid => $role) { - if (in_array($uid, $current, true)) { - $forum->set_moderator_role($uid, $post_id, $role); - } else { - $forum->add_moderator($uid, $post_id, $role); - } - } - return; - } - - // Fallback without forum service: rewrite meta directly (no legacy keep). - $ids = array_values(array_unique(array_map('intval', array_keys($desired)))); - update_post_meta($post_id, '_aksrt_moderators', $ids); - update_post_meta($post_id, '_aksrt_moderator_roles', $desired); - } - - private function save_applications(): void { - $actions = isset($_POST['aksrt_plate_app']) && is_array($_POST['aksrt_plate_app']) - ? wp_unslash($_POST['aksrt_plate_app']) - : []; - if (!$actions || !class_exists('AKSRT\\Forum\\Forum')) { - return; - } - - $forum = Forum::get_instance(); - foreach ($actions as $application_id => $action) { - $application_id = sanitize_text_field((string) $application_id); - $action = sanitize_key((string) $action); - if ($application_id === '' || $action === 'keep') { - continue; - } - if ($action === 'approved') { - $forum->review_moderator_application($application_id, Forum::APPLICATION_APPROVED, '', get_current_user_id()); - } elseif ($action === 'rejected') { - $forum->review_moderator_application($application_id, Forum::APPLICATION_REJECTED, '', get_current_user_id()); - } - } - } - - /** - * @param mixed $raw - * @return list - */ - private function sanitize_role_list($raw): array { - if (!is_array($raw)) { - return []; - } - $valid = array_keys($this->wp_role_options()); - $out = []; - foreach ($raw as $role) { - $role = sanitize_key((string) $role); - if ($role !== '' && in_array($role, $valid, true)) { - $out[] = $role; - } - } - return array_values(array_unique($out)); - } - - /** - * @return array - */ - private function wp_role_options(): array { - $out = []; - if (!function_exists('wp_roles')) { - return $out; - } - foreach (wp_roles()->roles as $slug => $data) { - $out[sanitize_key((string) $slug)] = translate_user_role((string) ($data['name'] ?? $slug)); - } - return $out; - } -} diff --git a/src/AKSRT/Forum/ScoreService.php b/src/AKSRT/Forum/ScoreService.php deleted file mode 100644 index caeb75fc..00000000 --- a/src/AKSRT/Forum/ScoreService.php +++ /dev/null @@ -1,447 +0,0 @@ -forum = $forum; - } - - // ================================================================ - // Favorite (§7.3) - // ================================================================ - - public function is_favorited(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return false; - } - $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); - $favorites = is_array($favorites) ? $favorites : []; - return in_array($topic_id, $favorites, true); - } - - public function toggle_favorite(int $user_id, int $topic_id) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); - $favorites = is_array($favorites) ? $favorites : []; - $was_favorited = in_array($topic_id, $favorites, true); - - if ($was_favorited) { - $favorites = array_values(array_diff($favorites, [$topic_id])); - } else { - $favorites[] = $topic_id; - } - - update_user_meta($user_id, 'aksrt_favorite_forum_posts', $favorites); - - $count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); - $count = $was_favorited ? max(0, $count - 1) : $count + 1; - update_post_meta($topic_id, '_aksrt_favorite_count', $count); - $this->refresh_recommend_score($topic_id); - - return [ - 'favorited' => !$was_favorited, - 'count' => $count, - 'message' => $was_favorited - ? __('已取消收藏', 'aksrt') - : __('收藏成功', 'aksrt'), - ]; - } - - // ================================================================ - // Score (§7.3) - // ================================================================ - - public function score_topic(int $user_id, int $topic_id, int $amount) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - if ($amount === 0) { - return new \WP_Error('invalid_amount', __('评分值不能为零', 'aksrt')); - } - - $author_id = (int) $post->post_author; - if ($author_id === $user_id) { - return new \WP_Error('self_score', __('不能给自己的帖子评分', 'aksrt')); - } - - $config = $this->forum->get_config(); - $max_extra = (int) ($config['post_page']['score_extra_max'] ?? 5); - $max_deduct = (int) ($config['post_page']['score_deduct_max'] ?? 3); - - if ($amount > 0 && $amount > $max_extra) { - $amount = $max_extra; - } - if ($amount < 0 && abs($amount) > $max_deduct) { - $amount = -$max_deduct; - } - - $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); - $detail = is_array($detail) ? $detail : []; - - $user_key = (string) $user_id; - if (isset($detail[$user_key])) { - return new \WP_Error('already_scored', __('你已经对该帖评过分', 'aksrt')); - } - - $detail[$user_key] = [ - 'amount' => $amount, - 'time' => current_time('mysql'), - ]; - update_post_meta($topic_id, '_aksrt_score_detail', $detail); - - $total = (int) get_post_meta($topic_id, '_aksrt_score', true); - $total += $amount; - update_post_meta($topic_id, '_aksrt_score', $total); - do_action('aksrt_forum_topic_scored', $topic_id, (int) $author_id, (float) $amount); - - $reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true); - $reputation += $amount; - update_user_meta($author_id, 'aksrt_reputation', $reputation); - - if ($amount > 0) { - $credits_amount = (int) ($config['credits']['score_extra'] ?? 3); - if ($credits_amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_score_extra', $credits_amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - $daily = $this->today_credits_count($author_id, 'forum_score_extra'); - $daily_max = (int) ($config['credits']['daily_max'] ?? 50); - if ($daily < $daily_max) { - \AKSRT\Credits\Credits::add($author_id, $credits_amount, 'forum_score_extra', sprintf( - __('帖子「%s」被加分', 'aksrt'), - get_the_title($topic_id) - )); - } - } - } - } - - $this->refresh_recommend_score($topic_id); - $this->notify_topic_scored($topic_id, $user_id, $amount); - - return [ - 'score' => $total, - 'message' => $amount > 0 - ? __('加分成功', 'aksrt') - : __('减分成功', 'aksrt'), - ]; - } - - public function get_score_detail(int $topic_id): array { - $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); - $detail = is_array($detail) ? $detail : []; - - $entries = []; - foreach ($detail as $uid => $entry) { - $user = get_userdata((int) $uid); - $entries[] = [ - 'user_id' => (int) $uid, - 'display_name' => $user ? $user->display_name : '', - 'amount' => (int) ($entry['amount'] ?? 0), - 'time' => (string) ($entry['time'] ?? ''), - ]; - } - - return $entries; - } - - // ================================================================ - // Recommend score (§7.3) - // ================================================================ - - public function calculate_recommend_score(int $topic_id): float { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return 0.0; - } - - $views = (int) get_post_meta($topic_id, '_aksrt_views', true); - $favorites = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); - $comments = (int) get_comments([ - 'post_id' => $topic_id, - 'count' => true, - 'status' => 'approve', - ]); - $score = (int) get_post_meta($topic_id, '_aksrt_score', true); - - $w_views = 1.0; - $w_favorites = 5.0; - $w_comments = 3.0; - $w_score = 2.0; - - $raw = ($views * $w_views) - + ($favorites * $w_favorites) - + ($comments * $w_comments) - + (max(0, $score) * $w_score); - - $post_time = strtotime($post->post_date_gmt); - $age_hours = max(1, (time() - $post_time) / 3600); - $decay = pow($age_hours, 1.5); - $recommend = ($raw + 1) / $decay * 10000; - - return round($recommend, 2); - } - - public function refresh_recommend_score(int $topic_id): float { - $score = $this->calculate_recommend_score($topic_id); - update_post_meta($topic_id, '_aksrt_recommend_score', $score); - $this->maybe_reward_hot_topic($topic_id, $score); - return $score; - } - - private function maybe_reward_hot_topic(int $topic_id, float $score): void { - $config = $this->forum->get_config(); - $hot_threshold = (float) ($config['post_page']['hot_recommend_threshold'] - ?? $config['credits']['hot_recommend_threshold'] - ?? $config['other']['hot_topic_threshold'] - ?? 500); - if ($hot_threshold <= 0) { - $hot_threshold = 500.0; - } - if ($score < $hot_threshold) { - return; - } - if (get_post_meta($topic_id, '_aksrt_hot_rewarded', true)) { - return; - } - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - $author_id = (int) $post->post_author; - if ($author_id <= 0) { - return; - } - do_action('aksrt_forum_topic_hot', $topic_id, (int) $author_id); - update_post_meta($topic_id, '_aksrt_hot_rewarded', '1'); - $amount = (int) ($config['credits']['posts_hot'] ?? 5); - if ($amount > 0) { - if (class_exists('\AKSRT\Credits\PointsFree')) { - \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_posts_hot', $amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - } elseif (class_exists('\AKSRT\Credits\Credits')) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_topic', [ - 'type' => 'post', 'id' => $topic_id, - ]); - } - } - $this->notify($author_id, 'hot', sprintf( - __('你的帖子「%s」成为热帖!', 'aksrt'), get_the_title($topic_id) - ), [ - 'title' => __('热帖通知', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - public function batch_refresh_recommend_scores(int $limit = 100): int { - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'post_status' => 'publish', - 'posts_per_page' => $limit, - 'orderby' => 'modified', - 'order' => 'DESC', - 'fields' => 'ids', - 'no_found_rows' => true, - ]); - - $count = 0; - foreach ($query->posts as $post_id) { - $this->refresh_recommend_score((int) $post_id); - $count++; - } - - return $count; - } - - public function maybe_refresh_recommend_on_view(): void { - if (!is_singular('aksrt_forum_topic')) { - return; - } - $topic_id = get_the_ID(); - if (!$topic_id) { - return; - } - $last = (int) get_post_meta($topic_id, '_aksrt_recommend_refreshed', true); - if ((time() - $last) > 300) { - $this->refresh_recommend_score($topic_id); - update_post_meta($topic_id, '_aksrt_recommend_refreshed', time()); - } - } - - private function get_bbs_type_label(string $bbs_type): string { - $config = $this->forum->get_config(); - $names = $config['names'] ?? []; - $map = [ - Forum::BBS_TYPE_STANDARD => $names['posts'] ?? __('帖子', 'aksrt'), - Forum::BBS_TYPE_QUESTION => $names['question_badge'] ?? __('提问', 'aksrt'), - ]; - return $map[$bbs_type] ?? $map[Forum::BBS_TYPE_STANDARD]; - } - - private function get_question_status_label(int $status): string { - $config = $this->forum->get_config(); - $names = $config['names'] ?? []; - return $status === Forum::QUESTION_SOLVED - ? ($names['question_ok_badge'] ?? __('已解决', 'aksrt')) - : ($names['question_badge'] ?? __('提问', 'aksrt')); - } - - // ================================================================ - // Notifications (§7.8) - // ================================================================ - - public function notify(int $user_id, string $type, string $content, array $data = []): void { - if ($user_id <= 0 || !class_exists('\AKSRT\Messages\Message')) { - return; - } - \AKSRT\Messages\Message::get_instance()->send($user_id, $type, $content, $data); - } - - public function notify_topic_pending(int $topic_id, int $author_id): void { - $title = get_the_title($topic_id); - $config = $this->forum->get_config(); - $admin_users = $config['admin_users'] ?? []; - - foreach ($admin_users as $admin_id) { - $this->notify((int) $admin_id, 'posts', sprintf( - __('帖子「%s」待审核', 'aksrt'), $title - ), [ - 'title' => __('待审核帖子', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - } - - public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $operator_id) return; - - $title = get_the_title($topic_id); - $action_labels = [ - 'review' => __('审核状态已变更', 'aksrt'), - 'sticky' => __('置顶状态已变更', 'aksrt'), - 'featured' => __('精华状态已变更', 'aksrt'), - 'locked' => __('锁定状态已变更', 'aksrt'), - 'close' => __('帖子已关闭', 'aksrt'), - 'hide' => __('帖子已隐藏', 'aksrt'), - 'delete' => __('帖子已删除', 'aksrt'), - 'undelete' => __('帖子已恢复', 'aksrt'), - ]; - - if ($action === 'review') { - $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); - if ($review_status === Forum::REVIEW_REJECTED) { - $action_labels['review'] = __('审核未通过', 'aksrt'); - } elseif ($review_status === Forum::REVIEW_PENDING) { - $action_labels['review'] = __('已转为待审核', 'aksrt'); - } elseif ($review_status === Forum::REVIEW_APPROVED) { - $action_labels['review'] = __('审核已通过', 'aksrt'); - } - } - - $label = $action_labels[$action] ?? __('治理动作已执行', 'aksrt'); - $target_url = $action === 'delete' ? aksrt_get_forum_home_url() : get_permalink($topic_id); - $payload = [ - 'title' => $label, - 'target_url' => $target_url, - ]; - - $note = (string) get_post_meta($topic_id, '_aksrt_moderation_note', true); - if ($note !== '') { - $payload['reason'] = $note; - } - - $this->notify($author_id, 'system', sprintf( - __('你的帖子「%1$s」%2$s', 'aksrt'), $title, $label - ), $payload); - } - - public function notify_topic_reply(int $topic_id, int $commenter_id): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $commenter_id) return; - - $commenter = get_userdata($commenter_id); - $name = $commenter ? $commenter->display_name : __('有人', 'aksrt'); - $title = get_the_title($topic_id); - - $this->notify($author_id, 'comment', sprintf( - __('%1$s 回复了你的帖子「%2$s」', 'aksrt'), $name, $title - ), [ - 'title' => __('新回复', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void { - $post = get_post($topic_id); - if (!$post) return; - $author_id = (int) $post->post_author; - if ($author_id <= 0 || $author_id === $scorer_id) return; - - $title = get_the_title($topic_id); - $this->notify($author_id, 'system', sprintf( - __('你的帖子「%1$s」被%2$s %3$d 分', 'aksrt'), - $title, - $amount > 0 ? __('加', 'aksrt') : __('减', 'aksrt'), - abs($amount) - ), [ - 'title' => $amount > 0 ? __('帖子被加分', 'aksrt') : __('帖子被减分', 'aksrt'), - 'target_url' => get_permalink($topic_id), - ]); - } - - // ================================================================ - // Private helpers - // ================================================================ - - private function today_credits_count(int $user_id, string $reason): int { - if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) { - return 0; - } - - $table = \AKSRT\Core\DBMigrator::table('credit_log'); - if (!$table) { - return 0; - } - - global $wpdb; - $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, $reason, $today - )); - } -} diff --git a/src/AKSRT/Forum/TopicAdminList.php b/src/AKSRT/Forum/TopicAdminList.php deleted file mode 100644 index 9730cffa..00000000 --- a/src/AKSRT/Forum/TopicAdminList.php +++ /dev/null @@ -1,424 +0,0 @@ - $label) { - $new[$key] = $label; - if ($key === 'title') { - $new['aksrt_plate'] = __('版块', 'aksrt'); - $new['aksrt_sticky'] = __('置顶', 'aksrt'); - $new['aksrt_locked'] = __('锁定', 'aksrt'); - $new['aksrt_review'] = __('审核', 'aksrt'); - $new['aksrt_bbs_type']= __('类型', 'aksrt'); - $new['aksrt_visibility'] = __('可见性', 'aksrt'); - $new['aksrt_views'] = __('阅读', 'aksrt'); - } - } - return $new; - } - - public function sortable_columns(array $columns): array { - $columns['aksrt_sticky'] = 'aksrt_sticky'; - $columns['aksrt_views'] = 'aksrt_views'; - return $columns; - } - - public function render_column(string $column, int $post_id): void { - switch ($column) { - case 'aksrt_plate': - $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); - if ($plate_id > 0) { - $plate = get_post($plate_id); - echo $plate ? esc_html(get_the_title($plate)) : esc_html('#' . $plate_id); - } else { - // fallback board taxonomy - $terms = get_the_terms($post_id, 'aksrt_forum_board'); - if (is_array($terms) && $terms) { - echo esc_html($terms[0]->name); - } else { - echo '—'; - } - } - break; - - case 'aksrt_sticky': - $sticky = (int) get_post_meta($post_id, '_aksrt_sticky', true); - $map = [ - 0 => __('否', 'aksrt'), - 1 => __('版块', 'aksrt'), - 2 => __('超级', 'aksrt'), - 3 => __('全局', 'aksrt'), - ]; - echo esc_html($map[$sticky] ?? (string) $sticky); - echo ''; - break; - - case 'aksrt_locked': - $locked = (int) get_post_meta($post_id, '_aksrt_locked', true); - echo $locked ? esc_html__('是', 'aksrt') : esc_html__('否', 'aksrt'); - break; - - case 'aksrt_review': - $status = (string) get_post_meta($post_id, '_aksrt_review_status', true); - if ($status === '') { - $status = 'approved'; - } - $map = [ - 'approved' => __('已通过', 'aksrt'), - 'pending' => __('待审核', 'aksrt'), - 'rejected' => __('已拒绝', 'aksrt'), - ]; - echo esc_html($map[$status] ?? $status); - break; - - case 'aksrt_bbs_type': - $type = (string) (get_post_meta($post_id, '_aksrt_bbs_type', true) ?: 'standard'); - echo esc_html($type === 'question' ? __('提问', 'aksrt') : __('标准', 'aksrt')); - break; - - case 'aksrt_visibility': - $visibility = (string) (get_post_meta($post_id, '_aksrt_visibility', true) ?: 'public'); - $map = [ - 'public' => __('公开', 'aksrt'), - 'login' => __('登录', 'aksrt'), - 'comment' => __('评论', 'aksrt'), - 'role' => __('角色', 'aksrt'), - 'credits' => __('积分', 'aksrt'), - 'pay' => __('付费', 'aksrt'), - 'moderator' => __('版主', 'aksrt'), - ]; - echo esc_html($map[$visibility] ?? $visibility); - break; - - case 'aksrt_views': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_views', true)); - break; - } - } - - public function filters(string $post_type): void { - if ($post_type !== 'aksrt_forum_topic') { - return; - } - - $sticky = isset($_GET['aksrt_sticky']) ? sanitize_key((string) wp_unslash($_GET['aksrt_sticky'])) : ''; - $locked = isset($_GET['aksrt_locked']) ? sanitize_key((string) wp_unslash($_GET['aksrt_locked'])) : ''; - $review = isset($_GET['aksrt_review']) ? sanitize_key((string) wp_unslash($_GET['aksrt_review'])) : ''; - $plate = isset($_GET['aksrt_plate']) ? (int) $_GET['aksrt_plate'] : 0; - - echo ' '; - - echo ' '; - - echo ' '; - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => 200, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - if ($plates) { - echo ' '; - } - } - - public function apply_filters(\WP_Query $query): void { - if (!is_admin() || !$query->is_main_query()) { - return; - } - $post_type = $query->get('post_type'); - if ($post_type !== 'aksrt_forum_topic') { - return; - } - - $meta_query = (array) $query->get('meta_query'); - - if (isset($_GET['aksrt_sticky']) && $_GET['aksrt_sticky'] !== '') { - $meta_query[] = [ - 'key' => '_aksrt_sticky', - 'value' => (int) $_GET['aksrt_sticky'], - ]; - } - if (isset($_GET['aksrt_locked']) && $_GET['aksrt_locked'] !== '') { - $meta_query[] = [ - 'key' => '_aksrt_locked', - 'value' => (int) $_GET['aksrt_locked'], - ]; - } - if (!empty($_GET['aksrt_review'])) { - $review = sanitize_key((string) wp_unslash($_GET['aksrt_review'])); - if ($review === 'approved') { - $meta_query[] = [ - 'relation' => 'OR', - [ - 'key' => '_aksrt_review_status', - 'value' => 'approved', - ], - [ - 'key' => '_aksrt_review_status', - 'compare' => 'NOT EXISTS', - ], - [ - 'key' => '_aksrt_review_status', - 'value' => '', - ], - ]; - } else { - $meta_query[] = [ - 'key' => '_aksrt_review_status', - 'value' => $review, - ]; - } - } - if (!empty($_GET['aksrt_plate'])) { - $meta_query[] = [ - 'key' => '_aksrt_plate_id', - 'value' => (int) $_GET['aksrt_plate'], - ]; - } - - if ($meta_query) { - $query->set('meta_query', $meta_query); - } - - $orderby = $query->get('orderby'); - if ($orderby === 'aksrt_sticky') { - $query->set('meta_key', '_aksrt_sticky'); - $query->set('orderby', 'meta_value_num'); - } elseif ($orderby === 'aksrt_views') { - $query->set('meta_key', '_aksrt_views'); - $query->set('orderby', 'meta_value_num'); - } - } - - public function bulk_actions(array $actions): array { - $actions['aksrt_sticky_1'] = __('设为版块置顶', 'aksrt'); - $actions['aksrt_sticky_0'] = __('取消置顶', 'aksrt'); - $actions['aksrt_lock'] = __('锁定', 'aksrt'); - $actions['aksrt_unlock'] = __('解锁', 'aksrt'); - $actions['aksrt_approve'] = __('审核通过', 'aksrt'); - $actions['aksrt_reject'] = __('审核拒绝', 'aksrt'); - return $actions; - } - - public function handle_bulk_actions(string $redirect_to, string $action, array $post_ids): string { - $count = 0; - foreach ($post_ids as $post_id) { - $post_id = (int) $post_id; - if ($post_id <= 0 || get_post_type($post_id) !== 'aksrt_forum_topic') { - continue; - } - switch ($action) { - case 'aksrt_sticky_1': - update_post_meta($post_id, '_aksrt_sticky', 1); - $count++; - break; - case 'aksrt_sticky_0': - update_post_meta($post_id, '_aksrt_sticky', 0); - $count++; - break; - case 'aksrt_lock': - update_post_meta($post_id, '_aksrt_locked', 1); - $count++; - break; - case 'aksrt_unlock': - update_post_meta($post_id, '_aksrt_locked', 0); - $count++; - break; - case 'aksrt_approve': - update_post_meta($post_id, '_aksrt_review_status', 'approved'); - $count++; - break; - case 'aksrt_reject': - update_post_meta($post_id, '_aksrt_review_status', 'rejected'); - $count++; - break; - } - } - - return add_query_arg('aksrt_topic_bulk', $count, $redirect_to); - } - - public function bulk_notices(): void { - if (empty($_GET['aksrt_topic_bulk'])) { - return; - } - $count = (int) $_GET['aksrt_topic_bulk']; - echo '

    ' . esc_html(sprintf( - /* translators: %d count */ - __('已更新 %d 个帖子。', 'aksrt'), - $count - )) . '

    '; - } - - public function quick_edit_box(string $column_name, string $post_type): void { - if ($post_type !== 'aksrt_forum_topic' || $column_name !== 'aksrt_sticky') { - return; - } - ?> -
    -
    - - - - - -
    -
    - post_type !== 'aksrt_forum_topic') { - return; - } - ?> - - ID, '_aksrt_plate_id', true); - $sticky = (int) get_post_meta($post->ID, '_aksrt_sticky', true); - $locked = (int) get_post_meta($post->ID, '_aksrt_locked', true); - $featured = (int) get_post_meta($post->ID, '_aksrt_featured', true); - $review = (string) get_post_meta($post->ID, '_aksrt_review_status', true); - if ($review === '') { - $review = 'approved'; - } - $bbs_type = (string) (get_post_meta($post->ID, '_aksrt_bbs_type', true) ?: 'standard'); - $visibility = sanitize_key((string) (get_post_meta($post->ID, '_aksrt_visibility', true) ?: 'public')); - if (!in_array($visibility, self::VISIBILITY, true)) { - $visibility = 'public'; - } - - $credits_cost = (int) get_post_meta($post->ID, '_aksrt_credits_cost', true); - if ($credits_cost <= 0) { - // Legacy dual key. - $credits_cost = max(0, (int) get_post_meta($post->ID, '_aksrt_pay_credits', true)); - } - $pay_price = (float) get_post_meta($post->ID, '_aksrt_pay_price', true); - $pay_vip_price = get_post_meta($post->ID, '_aksrt_pay_vip_price', true); - $vip_set = ($pay_vip_price !== '' && $pay_vip_price !== null && $pay_vip_price !== false); - $vip_price_val = $vip_set ? (float) $pay_vip_price : ''; - $pay_vip1_price = get_post_meta($post->ID, '_aksrt_pay_vip1_price', true); - $pay_vip2_price = get_post_meta($post->ID, '_aksrt_pay_vip2_price', true); - $vip1_set = ($pay_vip1_price !== '' && $pay_vip1_price !== null && $pay_vip1_price !== false); - $vip2_set = ($pay_vip2_price !== '' && $pay_vip2_price !== null && $pay_vip2_price !== false); - // Migrate single VIP price into vip1 display if vip1 empty. - if (!$vip1_set && $vip_set) { - $vip1_set = true; - $pay_vip1_price = $pay_vip_price; - } - $credits_vip1 = get_post_meta($post->ID, '_aksrt_credits_vip1', true); - $credits_vip2 = get_post_meta($post->ID, '_aksrt_credits_vip2', true); - $credits_vip1_set = ($credits_vip1 !== '' && $credits_vip1 !== null && $credits_vip1 !== false); - $credits_vip2_set = ($credits_vip2 !== '' && $credits_vip2 !== null && $credits_vip2 !== false); - $virtual_sales = max(0, (int) get_post_meta($post->ID, '_aksrt_virtual_sales', true)); - $coupon_s = (int) get_post_meta($post->ID, '_aksrt_topic_coupon_s', true) === 1; - $pay_hide_part = (int) get_post_meta($post->ID, '_aksrt_pay_hide_part', true) === 1; - $view_roles = get_post_meta($post->ID, '_aksrt_visibility_roles', true); - if (!is_array($view_roles)) { - $view_roles = []; - } - $view_roles = array_values(array_filter(array_map('sanitize_key', $view_roles))); - $wp_roles = $this->wp_role_options(); - - $plates = get_posts([ - 'post_type' => 'aksrt_forum_plate', - 'post_status' => 'publish', - 'posts_per_page' => 200, - 'orderby' => 'title', - 'order' => 'ASC', - ]); - - $visibility_labels = [ - 'public' => __('公开', 'aksrt'), - 'login' => __('登录可见', 'aksrt'), - 'comment' => __('评论可见', 'aksrt'), - 'role' => __('指定角色', 'aksrt'), - 'credits' => __('支付积分后可见', 'aksrt'), - 'pay' => __('付费后可见', 'aksrt'), - 'moderator' => __('仅版主', 'aksrt'), - ]; - ?> -

    - - -

    -

    - - -

    -

    -
    - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - - -

    -

    - - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - - -

    -

    - - -

    -

    - - - -

    -

    - - -

    - post_type !== 'aksrt_forum_topic' || !current_user_can('edit_post', $post_id)) { - return; - } - - $plate_id = isset($_POST['aksrt_plate_id']) ? max(0, (int) $_POST['aksrt_plate_id']) : 0; - update_post_meta($post_id, '_aksrt_plate_id', $plate_id); - - $sticky = isset($_POST['aksrt_sticky']) ? max(0, min(3, (int) $_POST['aksrt_sticky'])) : 0; - update_post_meta($post_id, '_aksrt_sticky', $sticky); - update_post_meta($post_id, '_aksrt_locked', !empty($_POST['aksrt_locked']) ? 1 : 0); - update_post_meta($post_id, '_aksrt_featured', !empty($_POST['aksrt_featured']) ? 1 : 0); - - $review = isset($_POST['aksrt_review_status']) ? sanitize_key((string) wp_unslash($_POST['aksrt_review_status'])) : 'approved'; - if (!in_array($review, ['approved', 'pending', 'rejected'], true)) { - $review = 'approved'; - } - update_post_meta($post_id, '_aksrt_review_status', $review); - - $bbs_type = isset($_POST['aksrt_bbs_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_bbs_type'])) : 'standard'; - if (!in_array($bbs_type, ['standard', 'question'], true)) { - $bbs_type = 'standard'; - } - update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type); - - $this->save_visibility_meta($post_id); - - // Keep board taxonomy bridge roughly in sync when plate selected. - if ($plate_id > 0 && taxonomy_exists('aksrt_forum_board')) { - $slug = 'plate-' . $plate_id; - $term = get_term_by('slug', $slug, 'aksrt_forum_board'); - if (!$term) { - $title = get_the_title($plate_id) ?: $slug; - $created = wp_insert_term($title, 'aksrt_forum_board', ['slug' => $slug]); - if (!is_wp_error($created) && !empty($created['term_id'])) { - $term_id = (int) $created['term_id']; - } else { - $term_id = 0; - } - } else { - $term_id = (int) $term->term_id; - } - if ($term_id > 0) { - wp_set_object_terms($post_id, [$term_id], 'aksrt_forum_board', false); - } - } - } - - private function save_visibility_meta(int $post_id): void { - $visibility = isset($_POST['aksrt_visibility']) ? sanitize_key((string) wp_unslash($_POST['aksrt_visibility'])) : 'public'; - if (!in_array($visibility, self::VISIBILITY, true)) { - $visibility = 'public'; - } - update_post_meta($post_id, '_aksrt_visibility', $visibility); - - // Rewrite legacy dual allow_view so admin/runtime stay on one contract. - $allow_view_map = [ - 'public' => 'public', - 'login' => 'login', - 'comment' => 'login', - 'role' => 'public', - 'credits' => 'pay', - 'pay' => 'pay', - 'moderator' => 'moderator', - ]; - update_post_meta($post_id, '_aksrt_allow_view', $allow_view_map[$visibility] ?? 'public'); - - // Roles. - if ($visibility === 'role') { - $roles = $this->sanitize_role_list($_POST['aksrt_visibility_roles'] ?? []); - update_post_meta($post_id, '_aksrt_visibility_roles', $roles); - } else { - delete_post_meta($post_id, '_aksrt_visibility_roles'); - } - - // Credits (canonical + legacy dual-write). - if ($visibility === 'credits') { - $cost = isset($_POST['aksrt_credits_cost']) ? max(0, (int) wp_unslash($_POST['aksrt_credits_cost'])) : 0; - // Accept legacy field name if UI still posts it. - if ($cost <= 0 && isset($_POST['aksrt_pay_credits'])) { - $cost = max(0, (int) wp_unslash($_POST['aksrt_pay_credits'])); - } - update_post_meta($post_id, '_aksrt_credits_cost', $cost); - update_post_meta($post_id, '_aksrt_pay_credits', $cost); - - if (isset($_POST['aksrt_credits_vip1']) && (string) wp_unslash($_POST['aksrt_credits_vip1']) !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) wp_unslash($_POST['aksrt_credits_vip1']))); - } else { - delete_post_meta($post_id, '_aksrt_credits_vip1'); - } - if (isset($_POST['aksrt_credits_vip2']) && (string) wp_unslash($_POST['aksrt_credits_vip2']) !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) wp_unslash($_POST['aksrt_credits_vip2']))); - } else { - delete_post_meta($post_id, '_aksrt_credits_vip2'); - } - } else { - delete_post_meta($post_id, '_aksrt_credits_cost'); - delete_post_meta($post_id, '_aksrt_pay_credits'); - delete_post_meta($post_id, '_aksrt_credits_vip1'); - delete_post_meta($post_id, '_aksrt_credits_vip2'); - } - - // Cash pay. - if ($visibility === 'pay') { - $price = isset($_POST['aksrt_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_pay_price'])) : 0; - update_post_meta($post_id, '_aksrt_pay_price', $price); - - $vip1_written = false; - if (isset($_POST['aksrt_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_pay_vip1_price']) !== '') { - $vip1 = max(0, (float) wp_unslash($_POST['aksrt_pay_vip1_price'])); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip1); - // Keep legacy single VIP key in sync with VIP1 for older readers. - update_post_meta($post_id, '_aksrt_pay_vip_price', $vip1); - $vip1_written = true; - } else { - delete_post_meta($post_id, '_aksrt_pay_vip1_price'); - } - if (isset($_POST['aksrt_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_pay_vip2_price']) !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_pay_vip2_price']))); - } else { - delete_post_meta($post_id, '_aksrt_pay_vip2_price'); - } - // Legacy field if posted without vip1. - if (!$vip1_written && isset($_POST['aksrt_pay_vip_price']) && (string) wp_unslash($_POST['aksrt_pay_vip_price']) !== '') { - $vip = max(0, (float) wp_unslash($_POST['aksrt_pay_vip_price'])); - update_post_meta($post_id, '_aksrt_pay_vip_price', $vip); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip); - } elseif (!$vip1_written) { - delete_post_meta($post_id, '_aksrt_pay_vip_price'); - } - } else { - delete_post_meta($post_id, '_aksrt_pay_price'); - delete_post_meta($post_id, '_aksrt_pay_vip_price'); - delete_post_meta($post_id, '_aksrt_pay_vip1_price'); - delete_post_meta($post_id, '_aksrt_pay_vip2_price'); - } - - // Partial hide only meaningful for pay/credits; otherwise clear. - if (in_array($visibility, ['pay', 'credits'], true)) { - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($_POST['aksrt_pay_hide_part']) ? 1 : 0); - $virtual = isset($_POST['aksrt_virtual_sales']) ? max(0, (int) wp_unslash($_POST['aksrt_virtual_sales'])) : 0; - if ($virtual > 0) { - update_post_meta($post_id, '_aksrt_virtual_sales', $virtual); - } else { - delete_post_meta($post_id, '_aksrt_virtual_sales'); - } - if ($visibility === 'pay') { - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($_POST['aksrt_topic_coupon_s']) ? 1 : 0); - } else { - delete_post_meta($post_id, '_aksrt_topic_coupon_s'); - } - } else { - delete_post_meta($post_id, '_aksrt_pay_hide_part'); - delete_post_meta($post_id, '_aksrt_virtual_sales'); - delete_post_meta($post_id, '_aksrt_topic_coupon_s'); - } - } - - /** - * @param mixed $raw - * @return string[] - */ - private function sanitize_role_list($raw): array { - if (!is_array($raw)) { - return []; - } - $allowed = array_keys($this->wp_role_options()); - $out = []; - foreach ($raw as $role) { - $role = sanitize_key((string) $role); - if ($role !== '' && in_array($role, $allowed, true)) { - $out[] = $role; - } - } - return array_values(array_unique($out)); - } - - /** - * @return array - */ - private function wp_role_options(): array { - if (!function_exists('wp_roles')) { - return []; - } - $roles = wp_roles(); - if (!$roles || !is_array($roles->roles)) { - return []; - } - $out = []; - foreach ($roles->roles as $key => $data) { - $out[sanitize_key((string) $key)] = (string) ($data['name'] ?? $key); - } - return $out; - } -} diff --git a/src/AKSRT/Forum/TopicPay.php b/src/AKSRT/Forum/TopicPay.php deleted file mode 100644 index ed89f0ae..00000000 --- a/src/AKSRT/Forum/TopicPay.php +++ /dev/null @@ -1,853 +0,0 @@ -\d+)', [ - [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_preview'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - [ - 'methods' => 'POST', - 'callback' => [$this, 'rest_purchase'], - 'permission_callback' => [$this, 'check_rest_nonce'], - ], - ]); - } - - /** - * @param \WP_REST_Request $request - * @return true|\WP_Error - */ - 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_preview(\WP_REST_Request $request) { - $topic_id = (int) $request['id']; - $uid = get_current_user_id(); - $coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? '')); - $preview = $this->preview($topic_id, $uid, $coupon_code); - if (is_wp_error($preview)) { - return $preview; - } - return new \WP_REST_Response($preview, 200); - } - - public function rest_purchase(\WP_REST_Request $request) { - $topic_id = (int) $request['id']; - $uid = get_current_user_id(); - $post = get_post($topic_id); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($uid <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), - ]); - } - - $params = $request->get_json_params(); - if (!is_array($params)) { - $params = $request->get_params(); - } - - $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); - if ($payment_method === '' || $payment_method === 'cash') { - $payment_method = 'balance'; - } - // Guests cannot use balance. - if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - $coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? '')); - - return $this->rest_wrap_result( - $this->create_cash_order($topic_id, $uid, $payment_method, $coupon_code) - ); - } - - /** - * @param mixed $result - * @return \WP_REST_Response|\WP_Error - */ - private function rest_wrap_result($result) { - if (is_wp_error($result)) { - return $result; - } - if (empty($result['success'])) { - $code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed')); - $status = (int) ($result['status'] ?? 400); - return new \WP_Error( - $code !== '' ? $code : 'aksrt_pay_failed', - (string) ($result['message'] ?? __('支付失败', 'aksrt')), - array_merge( - ['status' => $status > 0 ? $status : 400], - is_array($result['data'] ?? null) ? $result['data'] : [] - ) - ); - } - return new \WP_REST_Response($result, 200); - } - - /** - * Cashier preview payload (content-pay field subset for cms-download). - * - * @return array|\WP_Error - */ - public function preview(int $topic_id, int $user_id = 0, string $coupon_code = '') { - $assert = $this->assert_topic($topic_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - $uid = $user_id > 0 ? $user_id : get_current_user_id(); - $coupon_code = sanitize_text_field($coupon_code); - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - - if ($uid <= 0 && !$allow_guest) { - return [ - 'post_id' => $topic_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'topic', - 'pay_type_label' => __('付费主题', 'aksrt'), - 'amount' => 0, - 'base' => 0, - 'list_amount' => 0, - 'payable' => 0, - 'currency' => '¥', - 'amount_label' => '', - 'base_label' => '', - 'list_amount_label' => '', - 'is_free' => false, - 'has_paid_access' => false, - 'need_login' => true, - 'allow_guest' => false, - 'need_vip' => false, - 'need_purchase' => true, - 'logged_in' => false, - 'balance' => 0, - 'balance_enough' => false, - 'payment_methods' => [], - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => false, - 'coupon_desc' => '', - 'coupon_code' => '', - 'coupon' => null, - 'coupon_error' => '', - 'coupon_discount' => 0, - 'coupon_discount_label' => '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - ]; - } - - $priced = $this->resolve_price($topic_id, $uid); - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - // Guests never get VIP free (vip_level=0). - if ($uid <= 0) { - $vip_free = false; - } - $has_paid = $this->user_has_paid($uid, $topic_id); - - $wallet_balance = 0.0; - if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) { - $wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid); - } - - $methods = function_exists('aksrt_list_cash_payment_methods') - ? aksrt_list_cash_payment_methods($uid > 0) - : []; - - $coupon_allowed = $this->is_coupon_allowed($topic_id); - $coupon_error = ''; - $coupon_discount = 0.0; - $coupon_meta = null; - $applied_code = ''; - $payable = $list_amount; - - if (!$vip_free && $list_amount > 0) { - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, $coupon_code, true); - if (is_wp_error($discounted)) { - $coupon_error = $discounted->get_error_message(); - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, '', true); - } - if (!is_wp_error($discounted)) { - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $payable = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null; - $applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? '')); - if ($coupon_error === '' && !empty($discounted['coupon_error'])) { - $coupon_error = (string) $discounted['coupon_error']; - } - } - } elseif ($vip_free) { - $payable = 0.0; - $list_amount = 0.0; - } - - $is_free = $vip_free || $payable <= 0; - $amount_label = $is_free - ? __('免费', 'aksrt') - : sprintf('¥%s', number_format($payable, 2, '.', '')); - $list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', '')); - $base_label = sprintf('¥%s', number_format($base, 2, '.', '')); - - return [ - 'post_id' => $topic_id, - 'title' => get_the_title($post), - 'permalink' => get_permalink($post), - 'pay_modo' => '0', - 'pay_type' => 'topic', - 'pay_type_label' => __('付费主题', 'aksrt'), - 'amount' => $payable, - 'base' => $base, - 'list_amount' => $list_amount > 0 ? $list_amount : $base, - 'list_amount_label' => $list_label, - 'payable' => $payable, - 'currency' => '¥', - 'amount_label' => $amount_label, - 'base_label' => $base_label, - 'is_free' => $is_free, - 'has_paid_access' => $has_paid, - 'paid_order' => null, - 'pending_order' => null, - 'need_login' => $uid <= 0 && !$allow_guest, - 'allow_guest' => $allow_guest, - 'need_vip' => false, - 'need_purchase' => !$has_paid && !$is_free, - 'logged_in' => $uid > 0, - 'balance' => $wallet_balance, - 'wallet_balance' => $wallet_balance, - 'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable), - 'payment_methods' => $methods, - 'login_url' => wp_login_url(get_permalink($post)), - 'vip_url' => home_url('/'), - 'credits_url' => home_url('/'), - 'coupon_allowed' => $coupon_allowed, - 'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '', - 'coupon_code' => $applied_code, - 'coupon' => $coupon_meta, - 'coupon_error' => $coupon_error, - 'coupon_discount' => $coupon_discount, - 'coupon_discount_label' => $coupon_discount > 0 - ? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', ''))) - : '', - 'rebate_discount' => 0, - 'rebate_discount_label' => '', - 'points_deduction_allowed' => false, - 'card_pass_allowed' => false, - 'price_type' => '', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'is_vip_price' => !empty($priced['is_vip_price']), - ]; - } - - /** - * Create cash order and invoke gateway. - * - * @return array|\WP_Error - */ - public function create_cash_order(int $topic_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') { - $assert = $this->assert_topic($topic_id); - if (is_wp_error($assert)) { - return $assert; - } - /** @var \WP_Post $post */ - $post = $assert['post']; - - $allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post); - if ($user_id <= 0 && !$allow_guest) { - return new \WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url(get_permalink($post)), - ]); - } - - if ($this->user_has_paid($user_id, $topic_id)) { - return new \WP_Error('aksrt_already_paid', __('已购买,可直接查看。', 'aksrt'), [ - 'status' => 400, - 'has_paid_access' => true, - 'reload' => true, - ]); - } - - $priced = $this->resolve_price($topic_id, $user_id); - $list_amount = round((float) ($priced['amount'] ?? 0), 2); - $base = round((float) ($priced['base'] ?? $list_amount), 2); - $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price'])); - if ($user_id <= 0) { - $vip_free = false; - } - $coupon_code = sanitize_text_field($coupon_code); - - // VIP free (no purchase price) — no coupon stack. Guests never hit this. - if ($vip_free) { - return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [ - 'list_amount' => 0.0, - 'payable' => 0.0, - 'coupon_code' => '', - 'coupon_discount' => 0.0, - 'coupon' => [], - 'message' => __('VIP 免费解锁成功', 'aksrt'), - 'paid_note' => __('VIP 免费解锁主题。', 'aksrt'), - ]); - } - - if ($list_amount <= 0) { - return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]); - } - - $discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $user_id, $coupon_code, false); - if (is_wp_error($discounted)) { - return $discounted; - } - - $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2); - $amount = round((float) ($discounted['payable'] ?? $list_amount), 2); - $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : []; - $coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code); - - // Free after coupon discount. - if ($amount <= 0) { - return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [ - 'list_amount' => $list_amount, - 'payable' => 0.0, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'message' => __('无需支付,已可访问。', 'aksrt'), - 'paid_note' => __('优惠后无需支付。', 'aksrt'), - ]); - } - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]); - } - - $payment_method = sanitize_key($payment_method); - if ($payment_method === '') { - $payment_method = $user_id > 0 ? 'balance' : 'alipay'; - } - if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) { - $payment_method = 'alipay'; - } - - $pending = $this->find_pending_topic_order($user_id, $topic_id); - $can_resume = false; - if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) { - $pending_meta = $this->decode_order_note_meta($pending); - $pending_coupon = (string) ($pending_meta['coupon_code'] ?? ''); - if (is_array($pending_meta['coupon'] ?? null)) { - $pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon); - } - if ($pending_coupon === $coupon_code_norm) { - $can_resume = true; - } - } - - if ($can_resume) { - $order_number = (string) ($pending['order_number'] ?? ''); - $order = $pending; - } else { - $note = wp_json_encode([ - 'topic_pay' => 1, - 'topic_id' => $topic_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => $base, - 'list_amount' => $list_amount, - 'amount' => $amount, - 'payable' => $amount, - 'coupon_code' => $coupon_code_norm, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'product_id' => $topic_id, - 'amount' => $amount, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]); - } - } - - $order['subject'] = sprintf( - /* translators: %s: topic title */ - __('购买论坛主题「%s」', 'aksrt'), - get_the_title($post) - ); - $order['payment_method'] = $payment_method; - $order['amount'] = $amount; - - if (!class_exists('\AKSRT\Payment\Gateway')) { - return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]); - } - - $resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method); - if (empty($resolved['gateway'])) { - return new \WP_Error( - (string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'), - (string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')), - ['status' => 400, 'order_number' => $order_number] - ); - } - - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [ - 'paid_note' => __('用户完成论坛主题购买支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return new \WP_Error( - (string) ($payment_result['code'] ?? 'aksrt_pay_failed'), - (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')), - ['status' => 402, 'order_number' => $order_number] - ); - } - - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => false, - 'poll' => true, - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => $amount, - 'pay_modo' => '0', - 'message' => $payment_result['message'], - 'reload' => true, - ], - ]; - } - - /** - * On paid: unlock topic (or guest cookie) + burn coupon (idempotent). - */ - public function on_order_paid(string $order_number, array $paid_order): void { - $product_type = sanitize_key((string) ($paid_order['product_type'] ?? '')); - if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) { - return; - } - - $topic_id = (int) ($paid_order['product_id'] ?? 0); - $user_id = (int) ($paid_order['user_id'] ?? 0); - if ($topic_id <= 0) { - return; - } - - $meta = $this->decode_order_note_meta($paid_order); - $is_topic_pay = !empty($meta['topic_pay']); - if (!$is_topic_pay) { - $post = get_post($topic_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_topic') { - return; - } - } - - $fulfilled = $user_id <= 0; - if ($user_id > 0) { - $this->mark_unlocked($user_id, $topic_id); - $fulfilled = class_exists('AKSRT\\Forum\\Forum') - ? Forum::get_instance()->has_user_unlocked_topic($user_id, $topic_id) - : in_array($topic_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_unlocked_topics', true)), true); - } elseif (function_exists('aksrt_set_guest_pay_cookies')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (is_array($full)) { - $paid_order = array_merge($full, $paid_order); - } - } - aksrt_set_guest_pay_cookies($paid_order); - } - $this->finalize_topic_order_side_effects($order_number, $paid_order); - if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('付费主题权益已发放。', 'aksrt')] - ); - } - } - - /** - * @return array{post:\WP_Post}|\WP_Error - */ - private function assert_topic(int $topic_id) { - $post = get_post($topic_id); - if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('aksrt_invalid', __('主题不存在', 'aksrt'), ['status' => 404]); - } - if ($post->post_status !== 'publish') { - return new \WP_Error('aksrt_invalid', __('主题不可购买', 'aksrt'), ['status' => 404]); - } - - $visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true)); - if ($visibility !== Forum::VISIBILITY_PAY) { - return new \WP_Error('aksrt_not_paid', __('该主题不是付费可见内容', 'aksrt'), ['status' => 400]); - } - - return ['post' => $post]; - } - - /** - * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int} - */ - private function resolve_price(int $topic_id, int $user_id): array { - if (class_exists('AKSRT\\Forum\\Forum')) { - return Forum::get_instance()->resolve_topic_pay_price($topic_id, $user_id); - } - return [ - 'amount' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)), - 'is_vip_price' => false, - 'is_free' => false, - 'label' => '', - 'base' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)), - 'vip_set' => false, - 'vip_level' => 0, - ]; - } - - private function user_has_paid(int $user_id, int $topic_id): bool { - if ($user_id <= 0) { - return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($topic_id); - } - if (class_exists('AKSRT\\Forum\\Forum')) { - return Forum::get_instance()->has_user_paid_topic($user_id, $topic_id); - } - if (class_exists('\AKSRT\Payment\Order')) { - return \AKSRT\Payment\Order::get_instance()->has_paid_access( - $user_id, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - $topic_id - ); - } - return false; - } - - private function mark_unlocked(int $user_id, int $topic_id): void { - if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_topic_unlocked')) { - Forum::get_instance()->mark_topic_unlocked($user_id, $topic_id); - return; - } - if ($user_id <= 0 || $topic_id <= 0) { - return; - } - $unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); - if (!is_array($unlocked)) { - $unlocked = []; - } - $unlocked[] = $topic_id; - update_user_meta($user_id, 'aksrt_unlocked_topics', array_values(array_unique(array_map('intval', $unlocked)))); - } - - private function is_coupon_allowed(int $topic_id): bool { - return (int) get_post_meta($topic_id, '_aksrt_topic_coupon_s', true) === 1; - } - - /** - * VIP list price → coupon → payable. No affiliate rebate on topics. - * - * @param array $priced - * @return array|\WP_Error - */ - private function apply_topic_cash_discounts(array $priced, int $topic_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) { - $list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2); - $pay = $list_amount; - $coupon_discount = 0.0; - $coupon_meta = []; - $coupon_error = ''; - $coupon_code = sanitize_text_field($coupon_code); - - if ($coupon_code !== '') { - if (!$this->is_coupon_allowed($topic_id)) { - $err = new \WP_Error('aksrt_coupon_not_allowed', __('当前主题不支持优惠码。', 'aksrt'), ['status' => 400]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } elseif (!class_exists('\AKSRT\Payment\Coupon')) { - $err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [ - 'user_id' => max(0, $user_id), - 'product_ids' => [$topic_id], - 'context' => 'topic', - ]); - if (empty($result['valid'])) { - $err = new \WP_Error( - 'aksrt_coupon_error', - (string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')), - ['status' => 400] - ); - if (!$soft_coupon) { - return $err; - } - $coupon_error = $err->get_error_message(); - $coupon_code = ''; - } else { - $coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2); - if ($coupon_discount > $pay) { - $coupon_discount = $pay; - } - $pay = round(max(0.0, $pay - $coupon_discount), 2); - $c = is_array($result['coupon'] ?? null) ? $result['coupon'] : []; - $coupon_meta = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? $coupon_code), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'discount' => $coupon_discount, - ]; - $coupon_code = (string) $coupon_meta['code']; - } - } - } - - return [ - 'list_amount' => $list_amount, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'coupon_code' => $coupon_code, - 'coupon_error' => $coupon_error, - 'payable' => round(max(0.0, $pay), 2), - ]; - } - - /** - * Burn coupon after paid (idempotent via coupon_usage.order_number). - */ - private function finalize_topic_order_side_effects(string $order_number, array $order): void { - $meta = $this->decode_order_note_meta($order); - if (empty($meta['topic_pay'])) { - return; - } - if (($meta['pay_modo'] ?? '') === 'points') { - return; - } - - $buyer_id = (int) ($order['user_id'] ?? 0); - if (function_exists('aksrt_burn_order_coupon')) { - aksrt_burn_order_coupon($order_number, $meta, $buyer_id); - } - } - - /** - * Free VIP or free-after-discount paid order. - * - * @param array{ - * list_amount?:float, - * payable?:float, - * coupon_code?:string, - * coupon_discount?:float, - * coupon?:array, - * message?:string, - * paid_note?:string - * } $opts - * @return array|\WP_Error - */ - private function create_free_paid_order(int $topic_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) { - $list_amount = round((float) ($opts['list_amount'] ?? 0), 2); - $payable = round((float) ($opts['payable'] ?? 0), 2); - $coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? '')); - $coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2); - $coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : []; - $message = (string) ($opts['message'] ?? __('VIP 免费解锁成功', 'aksrt')); - $paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费解锁主题。', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) { - $this->mark_unlocked($user_id, $topic_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $note = wp_json_encode([ - 'topic_pay' => 1, - 'topic_id' => $topic_id, - 'pay_modo' => '0', - 'vip_level' => (int) ($priced['vip_level'] ?? 0), - 'base' => (float) ($priced['base'] ?? 0), - 'list_amount' => $list_amount, - 'amount' => $payable, - 'payable' => $payable, - 'coupon_code' => $coupon_code, - 'coupon_discount' => $coupon_discount, - 'coupon' => $coupon_meta, - 'free' => 1, - 'guest' => $user_id <= 0 ? 1 : 0, - 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => max(0, $user_id), - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'product_id' => $topic_id, - 'amount' => 0.0, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => 'free', - 'note' => is_string($note) ? $note : '', - 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), - ]); - - if ($order_number === '') { - $this->mark_unlocked($user_id, $topic_id); - return [ - 'success' => true, - 'data' => [ - 'order_number' => '', - 'amount' => 0, - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - $paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'free', - 'payment_transaction_id' => 'free_' . $order_number, - 'note' => $paid_note, - ]); - if (!$paid) { - return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]); - } - - $this->mark_unlocked($user_id, $topic_id); - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'amount' => 0, - 'pay_modo' => '0', - 'free' => true, - 'message' => $message, - 'reload' => true, - ], - ]; - } - - private function find_pending_topic_order(int $user_id, int $topic_id): ?array { - if ($user_id <= 0 || $topic_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) { - return null; - } - $orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [ - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, - 'status' => \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING, - 'limit' => 20, - 'offset' => 0, - ]); - foreach ($orders as $order) { - if ((int) ($order['product_id'] ?? 0) !== $topic_id) { - continue; - } - $meta = $this->decode_order_note_meta($order); - if (!empty($meta['topic_pay']) || $this->is_forum_topic_id($topic_id)) { - return $order; - } - } - return null; - } - - private function is_forum_topic_id(int $topic_id): bool { - $post = get_post($topic_id); - return $post instanceof \WP_Post && $post->post_type === 'aksrt_forum_topic'; - } - - private function decode_order_note_meta(array $order): array { - if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) { - return $order['meta_payload']; - } - $note = (string) ($order['note'] ?? ''); - if ($note === '') { - return []; - } - $decoded = json_decode($note, true); - return is_array($decoded) ? $decoded : []; - } -} diff --git a/src/AKSRT/Forum/TopicService.php b/src/AKSRT/Forum/TopicService.php deleted file mode 100644 index 0d13088f..00000000 --- a/src/AKSRT/Forum/TopicService.php +++ /dev/null @@ -1,275 +0,0 @@ -forum = $forum; - } - - // ================================================================ - // Topic CRUD - // ================================================================ - - /** - * Update topic content and editable governance metadata. - * - * @return bool|\WP_Error - */ - public function update_topic(int $topic_id, array $data) { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $update = ['ID' => $topic_id]; - - if (isset($data['title'])) { - $title = sanitize_text_field((string) $data['title']); - if ($title === '') { - return new \WP_Error('empty_title', __('标题不能为空', 'aksrt')); - } - $update['post_title'] = $title; - } - - if (isset($data['content'])) { - $content = wp_kses_post((string) $data['content']); - if ($content === '') { - return new \WP_Error('empty_content', __('内容不能为空', 'aksrt')); - } - $update['post_content'] = $content; - } - - if (isset($data['excerpt'])) { - $update['post_excerpt'] = sanitize_textarea_field((string) $data['excerpt']); - } - - $result = wp_update_post($update, true); - if (is_wp_error($result)) { - return $result; - } - - if (isset($data['tags']) && is_array($data['tags'])) { - $tags = array_values(array_filter(array_map('sanitize_text_field', $data['tags']))); - wp_set_object_terms($topic_id, $tags, 'aksrt_forum_tag'); - } - - $this->save_topic_governance_meta($topic_id, $data); - - return true; - } - - /** - * Create topic. - * - * @return array|\WP_Error - */ - public function create_topic(int $user_id, array $data) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); - } - - $board_id = (int) ($data['board_id'] ?? 0); - $title = sanitize_text_field((string) ($data['title'] ?? '')); - $content = wp_kses_post((string) ($data['content'] ?? '')); - $visibility = sanitize_key((string) ($data['visibility'] ?? Forum::VISIBILITY_PUBLIC)); - $special_topic = sanitize_text_field((string) ($data['special_topic'] ?? '')); - $tags = is_array($data['tags'] ?? null) ? $data['tags'] : []; - $bbs_type = sanitize_key((string) ($data['bbs_type'] ?? Forum::BBS_TYPE_STANDARD)); - if (!in_array($bbs_type, [Forum::BBS_TYPE_STANDARD, Forum::BBS_TYPE_QUESTION], true)) { - $bbs_type = Forum::BBS_TYPE_STANDARD; - } - - if ($board_id <= 0 || $title === '' || $content === '') { - return new \WP_Error('invalid_payload', __('请填写所有必填项', 'aksrt')); - } - - // §7.3: Strip unauthorized media from content based on compose capabilities - $content = $this->sanitize_topic_content($user_id, $content); - - $board_ok = $this->assert_board($board_id); - if (is_wp_error($board_ok)) { - return $board_ok; - } - - if (!isset($this->get_visibility_map()[$visibility])) { - $visibility = Forum::VISIBILITY_PUBLIC; - } - - $can_publish = $this->can_user_publish_directly($user_id, $board_id); - $post_status = $can_publish ? 'publish' : 'pending'; - $review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING; - - $post_id = wp_insert_post([ - 'post_title' => $title, - 'post_content' => $content, - 'post_status' => $post_status, - 'post_author' => $user_id, - 'post_type' => 'aksrt_forum_topic', - ], true); - - if (is_wp_error($post_id)) { - return $post_id; - } - - $this->attach_topic_to_board((int) $post_id, $board_id); - - $clean_tags = array_values(array_filter(array_map('sanitize_text_field', $tags))); - if (count($clean_tags) > 5) { - $clean_tags = array_slice($clean_tags, 0, 5); - } - if (!empty($clean_tags)) { - wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag'); - } - - $topic_cat = sanitize_text_field((string) ($data['topic_cat'] ?? '')); - if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) { - wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat'); - } - - update_post_meta($post_id, '_aksrt_visibility', $visibility); - update_post_meta($post_id, '_aksrt_review_status', $review_status); - - if ($visibility === Forum::VISIBILITY_CREDITS) { - $credits_cost = max(0, (int) ($data['credits_cost'] ?? 0)); - update_post_meta($post_id, '_aksrt_credits_cost', $credits_cost); - update_post_meta($post_id, '_aksrt_pay_credits', $credits_cost); - if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1'])); - } - if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') { - update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2'])); - } - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - } - if ($visibility === Forum::VISIBILITY_PAY) { - $pay_price = sanitize_text_field((string) ($data['pay_price'] ?? '')); - update_post_meta($post_id, '_aksrt_pay_price', $pay_price); - if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') { - $v1 = max(0, (float) $data['pay_vip1_price']); - update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1); - update_post_meta($post_id, '_aksrt_pay_vip_price', $v1); - } - if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') { - update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price'])); - } - update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0); - update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0); - } - if ($visibility === Forum::VISIBILITY_ROLE) { - $roles = isset($data['visibility_roles']) && is_array($data['visibility_roles']) - ? array_map('sanitize_key', $data['visibility_roles']) : []; - update_post_meta($post_id, '_aksrt_visibility_roles', $roles); - } - - update_post_meta($post_id, '_aksrt_special_topic', $special_topic); - // 置顶分级 0~3(§7.3):新建主题默认不置顶。 - update_post_meta($post_id, '_aksrt_sticky', Forum::STICKY_NONE); - update_post_meta($post_id, '_aksrt_featured', '0'); - update_post_meta($post_id, '_aksrt_locked', '0'); - update_post_meta($post_id, '_aksrt_views', 0); - update_post_meta($post_id, '_aksrt_last_reply', (string) get_post_field('post_date', $post_id)); - update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type); - if ($bbs_type === Forum::BBS_TYPE_QUESTION) { - update_post_meta($post_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED); - } - - $this->save_cover_meta($post_id, $data); - - $vote_meta = $this->prepare_vote_meta($data); - $this->save_vote_meta($post_id, $vote_meta); - - if ($post_status === 'publish') { - $this->forum->sync_board_count($board_id); - } - - if (class_exists('\AKSRT\Credits\Earn') && $post_status === 'publish') { - do_action('aksrt_forum_topic_created', $user_id, $post_id); - } - - if ($post_status === 'pending') { - $this->forum->notify_topic_pending($post_id, $user_id); - } - - return [ - 'post_id' => $post_id, - 'message' => $post_status === 'publish' - ? __('主题已发布', 'aksrt') - : __('主题已提交审核,审核通过后对外展示', 'aksrt'), - 'redirect' => $post_status === 'publish' - ? get_permalink($post_id) - : add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()), - ]; - } - - // ================================================================ - // Board / plate attachment helpers - // ================================================================ - - /** - * Validate board_id as plate CPT post ID when plate is active, else taxonomy term. - * - * @return true|\WP_Error - */ - private function assert_board(int $board_id, string $error_message = '') { - if ($error_message === '') { - $error_message = __('版块不存在', 'aksrt'); - } - - if ($board_id <= 0) { - return new \WP_Error('invalid_board', $error_message); - } - - if ($this->forum->is_plate_cpt_active()) { - $post = get_post($board_id); - if (!$post || $post->post_type !== 'aksrt_forum_plate' || $post->post_status !== 'publish') { - return new \WP_Error('invalid_board', $error_message); - } - return true; - } - - $board = get_term($board_id, 'aksrt_forum_board'); - if (!$board || is_wp_error($board)) { - return new \WP_Error('invalid_board', $error_message); - } - - return true; - } - - /** - * Attach a topic to a board. - * Plate CPT active: store `_aksrt_plate_id` only (native association). - * Otherwise: taxonomy term on aksrt_forum_board. - */ - private function attach_topic_to_board(int $topic_id, int $board_id): void { - if ($topic_id <= 0 || $board_id <= 0) { - return; - } - - if ($this->forum->is_plate_cpt_active()) { - update_post_meta($topic_id, '_aksrt_plate_id', $board_id); - return; - } - - if (taxonomy_exists('aksrt_forum_board')) { - wp_set_object_terms($topic_id, [$board_id], 'aksrt_forum_board'); - } - } - - // ================================================================ - // Topic moderation - // ================================================================ -} diff --git a/src/AKSRT/Forum/TopicServicePart1.php b/src/AKSRT/Forum/TopicServicePart1.php deleted file mode 100644 index 52f9327d..00000000 --- a/src/AKSRT/Forum/TopicServicePart1.php +++ /dev/null @@ -1,441 +0,0 @@ -post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - if (!$this->forum->can_moderate_topic($operator_id, $topic_id)) { - return new \WP_Error('forbidden', __('无权执行该治理动作', 'aksrt')); - } - - $action = sanitize_key($action); - $note = sanitize_textarea_field((string) ($payload['note'] ?? '')); - - switch ($action) { - case 'move_board': - $target_board_id = (int) ($payload['target_board_id'] ?? 0); - $target_ok = $this->assert_board($target_board_id, __('目标版块不存在', 'aksrt')); - if (is_wp_error($target_ok)) { - return $target_ok; - } - - if ($this->forum->is_plate_cpt_active()) { - $previous_plate_id = (int) $this->forum->get_topic_plate_id($topic_id); - $this->attach_topic_to_board($topic_id, $target_board_id); - if ($previous_plate_id > 0 && $previous_plate_id !== $target_board_id) { - $this->forum->sync_board_count($previous_plate_id); - } - $this->forum->sync_board_count($target_board_id); - } else { - $current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - if (is_wp_error($current_board_ids)) { - $current_board_ids = []; - } - $this->attach_topic_to_board($topic_id, $target_board_id); - foreach ($current_board_ids as $current_board_id) { - $this->forum->sync_board_count((int) $current_board_id); - } - $this->forum->sync_board_count($target_board_id); - } - break; - - case 'visibility': - $visibility = sanitize_key((string) ($payload['visibility'] ?? '')); - if (!isset($this->get_visibility_map()[$visibility])) { - return new \WP_Error('invalid_visibility', __('可见性参数无效', 'aksrt')); - } - update_post_meta($topic_id, '_aksrt_visibility', $visibility); - if ($visibility === Forum::VISIBILITY_CREDITS) { - update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($payload['credits_cost'] ?? 0))); - delete_post_meta($topic_id, '_aksrt_pay_price'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } elseif ($visibility === Forum::VISIBILITY_PAY) { - update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($payload['pay_price'] ?? ''))); - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } elseif ($visibility === Forum::VISIBILITY_ROLE) { - $roles = isset($payload['visibility_roles']) && is_array($payload['visibility_roles']) - ? array_values(array_filter(array_map('sanitize_key', $payload['visibility_roles']))) : []; - update_post_meta($topic_id, '_aksrt_visibility_roles', $roles); - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_pay_price'); - } else { - delete_post_meta($topic_id, '_aksrt_credits_cost'); - delete_post_meta($topic_id, '_aksrt_pay_price'); - delete_post_meta($topic_id, '_aksrt_visibility_roles'); - } - break; - - case 'review': - $review_status = sanitize_key((string) ($payload['review_status'] ?? '')); - if (!isset($this->get_review_map()[$review_status])) { - return new \WP_Error('invalid_review_status', __('审核状态参数无效', 'aksrt')); - } - - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - update_post_meta($topic_id, '_aksrt_review_status', $review_status); - if ($review_status === Forum::REVIEW_APPROVED) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - } elseif ($review_status === Forum::REVIEW_PENDING) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); - } else { - wp_update_post(['ID' => $topic_id, 'post_status' => 'draft']); - } - - foreach ($board_ids as $board_id) { - $this->forum->sync_board_count((int) $board_id); - } - break; - - case 'sticky': { - $raw = isset($payload['value']) ? $payload['value'] : null; - if ($raw === false || $raw === null) { - $level = Forum::STICKY_NONE; - } elseif (is_numeric($raw)) { - $level = (int) $raw; - } else { - $level = !empty($raw) ? Forum::STICKY_PLATE : Forum::STICKY_NONE; - } - - $level = max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, $level)); - update_post_meta($topic_id, '_aksrt_sticky', $level); - break; - } - - case 'featured': { - $prev = get_post_meta($topic_id, '_aksrt_featured', true) === '1'; - $next = !empty($payload['value']); - update_post_meta($topic_id, '_aksrt_featured', $next ? '1' : '0'); - - // 精华联动:从「未设精华」→「设为精华」时触发积分奖励 + 站内信通知(§7.3/§7.7/§7.8)。 - // 从「精华」→「取消」时不回收积分,仅不重复发放。 - if ($next && !$prev) { - $this->reward_essence($topic_id, $operator_id); - } - break; - } - - case 'locked': { - $locked = !empty($payload['value']); - update_post_meta($topic_id, '_aksrt_locked', $locked ? '1' : '0'); - wp_update_post([ - 'ID' => $topic_id, - 'comment_status' => $locked ? 'closed' : 'open', - ]); - break; - } - - case 'question_status': { - $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); - if ($bbs_type !== Forum::BBS_TYPE_QUESTION) { - return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); - } - $status = (int) ($payload['value'] ?? Forum::QUESTION_UNSOLVED); - $status = in_array($status, [Forum::QUESTION_UNSOLVED, Forum::QUESTION_SOLVED], true) - ? $status : Forum::QUESTION_UNSOLVED; - update_post_meta($topic_id, '_aksrt_question_status', $status); - break; - } - - case 'close': { - $closed = !empty($payload['value']); - if ($closed) { - update_post_meta($topic_id, '_aksrt_closed', '1'); - wp_update_post(['ID' => $topic_id, 'comment_status' => 'closed']); - } else { - delete_post_meta($topic_id, '_aksrt_closed'); - $locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1'; - if (!$locked) { - wp_update_post(['ID' => $topic_id, 'comment_status' => 'open']); - } - } - break; - } - - case 'hide': { - $hidden = !empty($payload['value']); - if ($hidden) { - update_post_meta($topic_id, '_aksrt_hidden', '1'); - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_update_post(['ID' => $topic_id, 'post_status' => 'private']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - } else { - delete_post_meta($topic_id, '_aksrt_hidden'); - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - } - break; - } - - case 'delete': { - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - wp_trash_post($topic_id); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - $author_id = (int) $post->post_author; - if ($author_id > 0 && $author_id !== $operator_id) { - $this->forum->notify($author_id, 'system', sprintf( - __('你的帖子「%s」已被删除', 'aksrt'), - $post->post_title - ), [ - 'title' => __('帖子已删除', 'aksrt'), - 'target_url' => aksrt_get_forum_home_url(), - 'reason' => $note, - ]); - } - break; - } - - case 'undelete': { - if ($post->post_status !== 'trash') { - return new \WP_Error('not_trashed', __('该帖不在回收站中', 'aksrt')); - } - wp_untrash_post($topic_id); - $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); - if ($review_status === Forum::REVIEW_APPROVED) { - wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); - } else { - wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); - } - $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); - foreach ($board_ids as $bid) { - $this->forum->sync_board_count((int) $bid); - } - break; - } - - default: - return new \WP_Error('invalid_action', __('不支持的治理动作', 'aksrt')); - } - - if ($note !== '') { - update_post_meta($topic_id, '_aksrt_moderation_note', $note); - } - - update_post_meta($topic_id, '_aksrt_moderated_by', $operator_id); - update_post_meta($topic_id, '_aksrt_moderated_at', current_time('mysql')); - - $this->forum->notify_moderation_result($topic_id, $action, $operator_id); - - return [ - 'message' => __('治理动作已执行', 'aksrt'), - 'topic' => $this->build_topic_payload(get_post($topic_id), $operator_id), - ]; - } - - /** - * 精华联动奖励(§7.3 联动 + §7.7 被设精华奖励 + §7.8 精华通知)。 - * - * - 触发积分奖励:读 `credits.essence` 配置,有每日上限与去重 meta(防止重复发放)。 - * - 触发站内信通知作者(依赖 \AKSRT\Messages\Message inbox 子系统)。 - * - 缺失依赖(积分模块未启用/未配置)时静默跳过,不阻塞治理动作。 - */ - public function reward_essence(int $topic_id, int $operator_id): void { - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return; - } - - $author_id = (int) $post->post_author; - if ($author_id <= 0) { - return; - } - - // 去重:同一帖已发过精华奖励则跳过(取消再加不重复发)。 - if (get_post_meta($topic_id, '_aksrt_essence_rewarded', true) === '1') { - return; - } - - // 积分奖励(§7.7):统一走免费积分日上限。 - if (class_exists('\AKSRT\Settings\Settings')) { - $cfg = \AKSRT\Settings\Settings::get_forum_settings(); - $amount = (int) ($cfg['credits']['essence'] ?? 0); - if ($amount > 0) { - $granted = 0; - if (class_exists('\AKSRT\Credits\PointsFree')) { - $result = \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_essence', $amount, [ - 'type' => 'post', - 'id' => $topic_id, - ]); - $granted = is_wp_error($result) ? 0 : (int) $result; - } elseif (class_exists('\AKSRT\Credits\Credits')) { - $daily_max = (int) ($cfg['credits']['daily_max'] ?? 50); - if ($daily_max > 0 && $this->today_credits_count($author_id, 'forum_essence') >= $daily_max) { - $amount = 0; - } - if ($amount > 0) { - \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_essence', [ - 'type' => 'post', - 'id' => $topic_id, - ], __('主题被设为精华', 'aksrt')); - $granted = $amount; - } - } - if ($granted > 0) { - update_post_meta($topic_id, '_aksrt_essence_rewarded', '1'); - update_post_meta($topic_id, '_aksrt_essence_rewarded_at', current_time('mysql')); - } - } - } - - // 站内信通知作者(§7.8)。 - if (class_exists('\AKSRT\Messages\Message')) { - $title = get_the_title($topic_id); - $permalink = (string) get_permalink($topic_id); - \AKSRT\Messages\Message::get_instance()->send( - $author_id, - 'forum_essence', - sprintf(__('你的主题「%s」被设为精华', 'aksrt'), $title), - [ - 'title' => __('主题被设为精华', 'aksrt'), - 'target_url' => $permalink, - ] - ); - } - - do_action('aksrt_forum_topic_featured', $topic_id, $author_id, $operator_id); - } - - /** - * 今日某 reason 的积分发放次数(用于精华/被加分等每日上限判断)。 - */ - public function today_credits_count(int $user_id, string $reason): int { - if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) { - return 0; - } - - $table = \AKSRT\Core\DBMigrator::table('credit_log'); - if (!$table) { - return 0; - } - - global $wpdb; - $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, $reason, $today - )); - } - - /** - * 置顶分级 label(对标 zibll zib_bbs_get_topping_badge)。 - */ - public function get_sticky_label(int $level): string { - switch ($level) { - case Forum::STICKY_GLOBAL: - return __('全局置顶', 'aksrt'); - case Forum::STICKY_SUPER: - return __('超级置顶', 'aksrt'); - case Forum::STICKY_PLATE: - return __('置顶', 'aksrt'); - default: - return ''; - } - } - - /** - * 读取并规范化置顶分级 meta。 - * - * 兼容历史 0/1 二态 meta("1" → STICKY_PLATE),新数据存 0~3 整数。 - */ - public function resolve_sticky_level(int $topic_id): int { - $raw = get_post_meta($topic_id, '_aksrt_sticky', true); - if ($raw === '' || $raw === null) { - return Forum::STICKY_NONE; - } - - if (is_numeric($raw)) { - return max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, (int) $raw)); - } - - return $raw === '1' ? Forum::STICKY_PLATE : Forum::STICKY_NONE; - } - - // ================================================================ - // Voting system - // ================================================================ - - /** - * Submit vote. - * - * @return array|\WP_Error - */ - public function submit_vote(int $topic_id, int $user_id, array $option_ids) { - if ($user_id <= 0) { - return new \WP_Error('not_logged_in', __('请先登录后投票', 'aksrt')); - } - - $post = get_post($topic_id); - if (!$post || $post->post_type !== 'aksrt_forum_topic') { - return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); - } - - $vote = $this->get_vote_payload($topic_id, $user_id); - if (empty($vote['enabled'])) { - return new \WP_Error('vote_disabled', __('当前主题未开启投票', 'aksrt')); - } - - if (!empty($vote['expired'])) { - return new \WP_Error('vote_expired', __('投票已结束', 'aksrt')); - } - - if (!empty($vote['has_voted'])) { - return new \WP_Error('already_voted', __('你已经参与过本次投票', 'aksrt')); - } - - $selected = array_values(array_unique(array_filter(array_map('sanitize_text_field', $option_ids)))); - if (empty($selected)) { - return new \WP_Error('empty_selection', __('请选择至少一个投票选项', 'aksrt')); - } - - if (empty($vote['multiple']) && count($selected) > 1) { - return new \WP_Error('too_many_selection', __('当前投票仅支持单选', 'aksrt')); - } - - $options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $options = is_array($options) ? $options : []; - $valid_option_ids = wp_list_pluck($options, 'id'); - - foreach ($selected as $selected_id) { - if (!in_array($selected_id, $valid_option_ids, true)) { - return new \WP_Error('invalid_option', __('投票选项不存在', 'aksrt')); - } - } - - $records = get_post_meta($topic_id, '_aksrt_vote_records', true); - $records = is_array($records) ? $records : []; - $records[(string) $user_id] = $selected; - update_post_meta($topic_id, '_aksrt_vote_records', $records); - - foreach ($options as &$option) { - if (in_array((string) ($option['id'] ?? ''), $selected, true)) { - $option['count'] = max(0, (int) ($option['count'] ?? 0)) + 1; - } - } - unset($option); - - update_post_meta($topic_id, '_aksrt_vote_options', $options); - - return [ - 'message' => __('投票成功', 'aksrt'), - 'vote' => $this->get_vote_payload($topic_id, $user_id), - ]; - } -} diff --git a/src/AKSRT/Forum/TopicServicePart2.php b/src/AKSRT/Forum/TopicServicePart2.php deleted file mode 100644 index a6e285a9..00000000 --- a/src/AKSRT/Forum/TopicServicePart2.php +++ /dev/null @@ -1,382 +0,0 @@ -forum->get_config(); - $vote_max = (int) ($config['other']['vote_max'] ?? 8); - if ($vote_max > 0 && count($labels) > $vote_max) { - $labels = array_slice($labels, 0, $vote_max); - } - - $vote_type = sanitize_key((string) ($data['vote_type'] ?? 'standard')); - if ($vote_type === 'pk' && count($labels) !== 2) { - $vote_type = 'standard'; - } - if ($vote_type === 'pk') { - $multiple = false; - } - - if (count($labels) < 2) { - $enabled = false; - } - - $expiry_days = (int) ($data['vote_expiry_days'] ?? 0); - - $options = []; - foreach ($labels as $index => $label) { - $options[] = [ - 'id' => 'option_' . ($index + 1), - 'label' => $label, - 'count' => 0, - ]; - } - - return [ - 'enabled' => $enabled, - 'title' => $title, - 'multiple' => $multiple, - 'vote_type' => $vote_type, - 'expiry_days' => $expiry_days, - 'options' => $options, - ]; - } - - /** - * Save vote meta while preserving counts when possible. - */ - public function save_vote_meta(int $topic_id, array $vote_meta): void { - $existing_options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $existing_options = is_array($existing_options) ? $existing_options : []; - $existing_by_label = []; - foreach ($existing_options as $existing_option) { - if (!empty($existing_option['label'])) { - $existing_by_label[(string) $existing_option['label']] = $existing_option; - } - } - - $options = []; - foreach ($vote_meta['options'] as $index => $option) { - $label = (string) ($option['label'] ?? ''); - if ($label === '') { - continue; - } - - $existing = $existing_by_label[$label] ?? null; - $options[] = [ - 'id' => $existing['id'] ?? ('option_' . ($index + 1)), - 'label' => $label, - 'count' => (int) ($existing['count'] ?? 0), - ]; - } - - update_post_meta($topic_id, '_aksrt_vote_enabled', !empty($vote_meta['enabled']) ? '1' : '0'); - update_post_meta($topic_id, '_aksrt_vote_title', sanitize_text_field((string) ($vote_meta['title'] ?? ''))); - update_post_meta($topic_id, '_aksrt_vote_multiple', !empty($vote_meta['multiple']) ? '1' : '0'); - update_post_meta($topic_id, '_aksrt_vote_options', $options); - $vote_type = in_array($vote_meta['vote_type'] ?? '', ['standard', 'pk'], true) ? $vote_meta['vote_type'] : 'standard'; - update_post_meta($topic_id, '_aksrt_vote_type', $vote_type); - $expiry_days = (int) ($vote_meta['expiry_days'] ?? 0); - if (!in_array($expiry_days, [0, 1, 7, 30], true)) { - $expiry_days = 0; - } - update_post_meta($topic_id, '_aksrt_vote_expiry_days', $expiry_days); - if ($expiry_days > 0 && !get_post_meta($topic_id, '_aksrt_vote_started_at', true)) { - update_post_meta($topic_id, '_aksrt_vote_started_at', current_time('mysql')); - } - - if (empty($vote_meta['enabled'])) { - delete_post_meta($topic_id, '_aksrt_vote_records'); - } - } - - /** - * Get vote payload for a topic. - */ - public function get_vote_payload(int $topic_id, int $user_id = 0): array { - $enabled = get_post_meta($topic_id, '_aksrt_vote_enabled', true) === '1'; - $options = get_post_meta($topic_id, '_aksrt_vote_options', true); - $options = is_array($options) ? $options : []; - $records = get_post_meta($topic_id, '_aksrt_vote_records', true); - $records = is_array($records) ? $records : []; - - $vote_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_vote_type', true)); - if (!in_array($vote_type, ['standard', 'pk'], true)) { - $vote_type = 'standard'; - } - $expiry_days = (int) get_post_meta($topic_id, '_aksrt_vote_expiry_days', true); - $started_at = (string) get_post_meta($topic_id, '_aksrt_vote_started_at', true); - $expired = false; - if ($expiry_days > 0 && $started_at !== '') { - $expire_time = strtotime($started_at) + ($expiry_days * DAY_IN_SECONDS); - $expired = time() >= $expire_time; - } - - $config = $this->forum->get_config(); - $display_mode = $config['other']['vote_number_type'] ?? 'percent'; - - $selected = $user_id > 0 && isset($records[(string) $user_id]) && is_array($records[(string) $user_id]) - ? array_values(array_map('strval', $records[(string) $user_id])) - : []; - $total_votes = 0; - - foreach ($options as &$option) { - $count = max(0, (int) ($option['count'] ?? 0)); - $option['count'] = $count; - $option['selected'] = in_array((string) ($option['id'] ?? ''), $selected, true); - $total_votes += $count; - } - unset($option); - - return [ - 'enabled' => $enabled && !empty($options), - 'title' => (string) get_post_meta($topic_id, '_aksrt_vote_title', true), - 'multiple' => get_post_meta($topic_id, '_aksrt_vote_multiple', true) === '1', - 'vote_type' => $vote_type, - 'expiry_days' => $expiry_days, - 'expired' => $expired, - 'display_mode' => $display_mode, - 'options' => $options, - 'total_votes' => $total_votes, - 'has_voted' => !empty($selected), - 'selected' => $selected, - ]; - } - - // ================================================================ - // Content processing - // ================================================================ - - /** - * Save governance meta fields from an editable payload. - */ - public function save_topic_governance_meta(int $topic_id, array $data): void { - if (isset($data['visibility'])) { - $visibility = sanitize_key((string) $data['visibility']); - if (isset($this->get_visibility_map()[$visibility])) { - update_post_meta($topic_id, '_aksrt_visibility', $visibility); - } - } - - if (array_key_exists('special_topic', $data)) { - update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $data['special_topic'])); - } - - if (array_key_exists('vote_enabled', $data) || array_key_exists('vote_options', $data) || array_key_exists('vote_options_text', $data)) { - $this->save_vote_meta($topic_id, $this->prepare_vote_meta($data)); - } - } - - public function save_cover_meta(int $post_id, array $data): void { - $cover_type = sanitize_key((string) ($data['cover_type'] ?? '')); - if (!in_array($cover_type, ['image', 'slide', 'video'], true)) { - delete_post_meta($post_id, '_aksrt_cover_type'); - return; - } - - update_post_meta($post_id, '_aksrt_cover_type', $cover_type); - - if ($cover_type === 'image') { - $url = esc_url_raw((string) ($data['cover_image'] ?? '')); - update_post_meta($post_id, '_aksrt_cover_image', $url); - } elseif ($cover_type === 'slide') { - $raw = $data['featured_slide'] ?? ''; - $slides = is_array($raw) ? $raw : json_decode((string) $raw, true); - if (!is_array($slides)) { - $slides = []; - } - $slides = array_values(array_filter(array_map('esc_url_raw', array_slice($slides, 0, 9)))); - update_post_meta($post_id, '_aksrt_featured_slide', $slides); - } elseif ($cover_type === 'video') { - $url = esc_url_raw((string) ($data['featured_video'] ?? '')); - update_post_meta($post_id, '_aksrt_featured_video', $url); - } - } - - /** - * Sanitize topic content based on user's compose capabilities. - * - * Strips ,