feat(ban): mirror reference theme ban/restriction prompt surface

修复公开主页禁封提示框为空的问题,并对齐参考主题(Zibll)全部前台禁封提示入口:
- fix(profile): 资料卡下方治理提示框渲染为空——模板读取的 tone/message 键与
  数据层实际输出的 state/label/text 契约不匹配;现按禁封类型展示
  「违规已封号/小黑屋禁封中/禁言中」+ 可配置说明文案
- feat(ban): 用户中心页头展示治理状态徽章 + 查看详情入口,直达禁封申诉页
  (参考主题 user_page_header_desc / zib_get_user_ban_info_link)
- feat(ban): 评论拒绝态区分禁封/小黑屋/禁言,附状态徽章与申诉链接
  (参考主题 zib_get_user_ban_nocan_info,原为笼统的无权限文案)
- feat(ban): 禁封申诉页补全状态信息卡(状态/开始时间/结束时间/原因/说明/
  申诉权限),对齐 zib_get_ban_info_modal;此前仅有「禁封原因」一行且无入口
- feat(ban): 昵称旁禁封徽章 + 头像禁封角标(覆盖 VIP 位,参考主题
  user_name_badge / user_avatar_badge 钩子语义),main.css 与入队的
  main.min.css 同步新增角标样式
- fix(ban): 举报入口对已封禁/小黑屋目标隐藏(评论、文章下拉、封面菜单三处,
  对齐 zib_get_report_link 排除语义;禁言为轻量限制不隐藏)
- refactor(ban): 治理展示态收敛为单一数据源 Restriction::get_display_state()
  + state_labels(),资料卡/用户中心/评论拒绝态/昵称徽章共用
- feat(ban): 小黑屋用户开放申诉(对齐参考主题 zib_user_is_ban 双类型语义),
  禁封站内信附加申诉页链接
- test: report-ban 契约测试 +3(展示态单一数据源、提示面入口矩阵、举报排除),
  45 个测试文件全部通过
This commit is contained in:
2026-09-22 13:28:52 +08:00
parent 8d9cca2fe5
commit 7f520803ff
17 changed files with 457 additions and 46 deletions
@@ -19,6 +19,16 @@ const functions = read('functions.php')
const uninstall = read('uninstall.php')
const userBanHelper = read('inc/helpers/user-ban.php')
const userBanJs = read('assets/js/user-ban.js')
const authorHero = read('templates/parts/user/author-hero.php')
const ucHeader = read('inc/helpers/user-center-renderers/profile-header.php')
const commentsHelper = read('inc/helpers/aksrt-comments.php')
const appealPage = read('templates/pages/page-ban-appeal.php')
const identityHelper = read('inc/helpers/aksrt-identity.php')
const avatarHelper = read('inc/helpers/aksrt-avatar.php')
const singleComponents = read('inc/helpers/aksrt-single-components.php')
const banAppeal = read('src/User/BanAppeal.php')
const mainCss = read('assets/css/main.css')
const mainMinCss = read('assets/css/main.min.css')
test('report REST surface is canonical: submit + users/reports only', () => {
assert.match(userController, /'\/report\/submit'/)
@@ -130,9 +140,75 @@ test('ban appeal approval actually lifts restrictions', () => {
})
test('profile governance notice reads from the restriction table', () => {
assert.match(home, /->is_banned\(\$user_id\)/)
assert.match(home, /->get_display_state\(\$user_id\)/)
assert.match(home, /Restriction::state_labels\(\)\[\$state\]/)
assert.match(home, /\\AKSRT\\User\\Restriction::get_instance\(\)/)
assert.ok(!home.includes('aksrt_user_ban_state'))
// 提示框内容契约:state|label|text,label 对齐参考主题公开主页禁封徽章文案。
assert.match(home, /'state'\s*=> \$state/)
assert.match(home, /'label'\s*=>/)
assert.match(home, /'text'\s*=> \(string\) \$settings\['report'\]\['ban_notice_text'\]/)
// 模板必须消费真实契约键:tone/message 双键曾导致资料卡下方提示框渲染为空。
assert.match(authorHero, /\$notice\['state'\]/)
assert.match(authorHero, /\$notice\['text'\]/)
assert.match(authorHero, /\$notice\['label'\]/)
assert.ok(!authorHero.includes("$notice['message']"), 'template must not read the removed message key')
assert.ok(!authorHero.includes("$notice['tone']"), 'template must not read the removed tone key')
})
test('governance display state is a single source shared by every prompt surface', () => {
// 展示态与文案集中在 Restriction(资料卡/用户中心/评论拒绝态/昵称徽章共用)。
for (const state of ['STATE_BANNED', 'STATE_RESTRICTED', 'STATE_MUTED']) {
assert.match(restriction, new RegExp(`const ${state}\\s+=`), `missing display state: ${state}`)
}
assert.match(restriction, /public function get_display_state\(int \$user_id\): string/)
assert.match(restriction, /public static function state_labels\(\): array/)
for (const label of ["__('违规已封号', 'aksrt')", "__('小黑屋禁封中', 'aksrt')", "__('禁言中', 'aksrt')"]) {
assert.ok(restriction.includes(label), `missing state label: ${label}`)
}
assert.match(restriction, /is_banned\(\$user_id\)[\s\S]*?is_suspended\(\$user_id\)[\s\S]*?is_restricted\(\$user_id, self::TYPE_MUTE\)/)
// 请求级缓存:评论列表逐条渲染昵称徽章不得反复查库。
assert.match(userBanHelper, /static \$cache = \[\]/)
})
test('ban prompt surface mirrors the reference theme entry points', () => {
// 用户中心页头(参考主题 user_page_header_desc / zib_get_user_ban_info_link)。
assert.match(userBanHelper, /function aksrt_get_user_ban_notice_html/)
assert.match(userBanHelper, /function aksrt_get_ban_appeal_page_url/)
assert.match(ucHeader, /aksrt_get_user_ban_notice_html/)
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
assert.match(userBanHelper, /function aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /暂时无法发表评论/)
// 昵称徽章 + 头像角标(参考主题 user_name_badge / user_avatar_badge)。
assert.match(userBanHelper, /function aksrt_get_user_ban_name_badge/)
assert.match(userBanHelper, /function aksrt_get_user_ban_avatar_badge/)
assert.match(identityHelper, /aksrt_get_user_ban_name_badge\(\$user_id\)/)
assert.match(avatarHelper, /aksrt_get_user_ban_avatar_badge\(\$user_id\)/)
// 禁封申诉页完整状态信息(参考主题 zib_get_ban_info_modal)。
assert.match(appealPage, /get_active_suspend\(\$user_id\)/)
for (const row of ['状态', '开始时间', '结束时间', '原因', '说明']) {
assert.match(appealPage, new RegExp(`__\\('${row}', 'aksrt'\\)`), `appeal page missing status row: ${row}`)
}
assert.match(appealPage, /esc_html_e\('申诉权限', 'aksrt'\)/)
assert.match(appealPage, /禁封中\(禁止登录\)/)
assert.match(appealPage, /本次禁封您将不能发起申诉/)
// 禁封/小黑屋均可申诉(参考主题 zib_user_is_ban 双类型语义)。
assert.match(banAppeal, /STATE_BANNED, \\AKSRT\\User\\Restriction::STATE_RESTRICTED/)
// 禁封站内信附申诉入口(参考主题 zib_updata_user_ban_msg 申诉指引)。
assert.match(restrictionOps, /aksrt_get_ban_appeal_page_url/)
// 头像角标样式:源文件与入队的 min 产物同步。
for (const css of [mainCss, mainMinCss]) {
assert.ok(css.includes('aksrt-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
}
})
test('report entry hides targets already banned or suspended', () => {
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
assert.match(userBanHelper, /function aksrt_is_user_ban_restricted/)
assert.match(commentItem, /aksrt_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /aksrt_is_user_ban_restricted\(\$post_author_id\)/)
assert.match(authorHero, /aksrt_is_user_ban_restricted\(\$user_id\)/)
})
test('admin report notification links to the real page', () => {