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 @@
-
-
-
-
-
-
- 共 {{ boards.length }} 个版块
- 新增版块
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 保存
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
-
-
-
{{ stats[card.key] || 0 }}
-
{{ card.label }}
-
-
-
-
-
-
-
-
- 待审核主题
- 进入主题治理
-
-
-
-
-
-
-
-
-
-
- 待处理版主申请
- 进入申请处理
-
-
-
-
-
-
-
-
-
-
-
- 版块概况
- 进入版块管理
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
-
- 查询
- 重置
-
-
-
-
-
-
-
-
-
-
- {{ activeApplication?.display_name || '-' }} / {{ activeApplication?.board_name || '-' }}
- {{ activeApplication?.reason || '暂无申请理由' }}
-
-
- 通过
- 拒绝
-
-
-
-
-
-
- 取消
- 提交处理
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 待处理
-
-
-
-
-
-
-
-
-
-
- 已加载 {{ boards.length }} 个版块
-
- 查看版主申请
- 批量管理
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ moderator.display_name }}
- {{ getRoleLabel(moderator.role) }}
-
-
{{ moderator.user_email }}
-
- {{ getPermissionLabel(perm) }}
-
-
-
-
- 移除
-
-
-
-
-
-
-
-
-
- 添加版主
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 批量添加
- 批量移除
- 批量修改角色
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 执行
-
-
-
-
-
-
-
-
-
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 @@
-
-
-
-
-
-
- {{ detail.topic.title || '-' }}
- {{ primaryBoardName }}
- {{ detail.topic.author?.display_name || '-' }}
- {{ detail.topic.visibility?.label || '-' }}
- {{ detail.topic.review_status?.label || '-' }}
- {{ detail.topic.status || '-' }}
- {{ stickySummary }}
- {{ detail.topic.featured ? '是' : '否' }}
- {{ bbsTypeLabel }}
- {{ questionStatusLabel }}
- {{ detail.topic.favorite_count ?? 0 }}
- {{ detail.topic.score ?? 0 }}
- {{ detail.topic.special_topic || '-' }}
- {{ voteSummary }}
- {{ detail.topic.moderation_note || '-' }}
-
-
-
-
内容摘要
-
{{ detail.topic.excerpt || '暂无摘要' }}
-
-
-
-
执行治理动作
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 执行
- 刷新详情
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 .= '';
-
- 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 .= '';
- 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 .= '';
- 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';
- ?>
-
- 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 '';
- }
-
- 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
,