feat(ban): align ban prompts with the reference theme modals and block page

对照参考主题(Zibll)修正四项偏差,申诉链路从页面整体迁移至弹窗:

- feat(ban): 新增三个 1:1 复刻弹窗(REST /user-ban/{info,appeal,log}-modal):
  查看禁封状态(多彩头 c-gray + 信息行 + 申诉按钮)、禁封申诉(edit-header
  gavel/c-blue + 信息框 + 资料项 data[key] 输入 + 审核中态 + 禁止申诉态)、
  禁封记录(多彩头 c-yellow + 计数 + 逐条卡片);user-ban.js 增加
  data-aksrt-ban-info/appeal/log 委托与 admin-ajax 申诉提交
- feat(ban): 禁封/解封弹窗的「当前用户已有N次禁封记录」静态文案改为可点击
  黄链(N>1),打开禁封记录弹窗;解封视图信息行改复用统一数据源
- refactor(ban): 治理展示态收敛至 Restriction::state_badges()(封号=c-red
  违规已封号 / 小黑屋=c-yellow 小黑屋禁封中 / 禁言=c-gray 禁言中,颜色与
  字段统一管理)+ get_ban_info()(状态/起止时间/原因/说明/申诉权限,
  三弹窗与禁封页共用)
- feat(ban): 登录行为对齐参考主题——移除登录期拦截(enforce_login_ban)
  与 complete_login 禁封闸门,封号用户可正常登录;新增 template_redirect
  前台禁封页(1:1 zib_user_ban_restrict_login/zib_die_page:红盒提示 +
  信息行 + 申诉权限 + 退出登录/申诉按钮,模板 templates/parts/user/ban-block.php)
- feat(profile): 公开主页禁封状态从资料卡下方提示框改为占据身份行元信息位
  的 badg 徽章(参考主题 author_header_identity 语义);用户中心元信息改为
  but 按钮(徽章+箭头)打开查看禁封状态弹窗;评论拒绝态链接同源
- refactor: 移除申诉页面模板(page-ban-appeal.php)及其资产/注册/入队、
  get_profile_notice/profile_notice 链、state_labels/state_notices/
  login_ban_message、UserRuntime 的 public_notice/ban_notice_text 死配置、
  report_settings 死键、无消费方的 aksrt_ban_appeal_status 端点
- feat(ban): BanAppeal::ajax_submit 兼容弹窗 data[key] 表单结构
  (逐项清洗入库,申诉说明取 data['申诉说明'],旧平铺字段保持兼容)
- chore: 重新构建 public.css/public.min.css(三个新弹窗几何与头部样式)
- test: 契约测试重写(徽章元信息位、三弹窗与记录链接、禁封页、允许登录 +
  前台拦截、死代码移除断言),全量 362 项通过
This commit is contained in:
2026-09-22 19:14:02 +08:00
parent 11eda33e58
commit a4af024f32
31 changed files with 1037 additions and 679 deletions
+66 -55
View File
@@ -38,8 +38,10 @@ class Restriction {
}
public function init(): void {
// Enforce bans on login
add_filter('authenticate', [$this, 'enforce_login_ban'], 999, 3);
// 禁封用户前台拦截(对齐参考主题 zib_user_ban_restrict_login:
// 封号用户可正常登录,但前台任何页面均展示禁封状态页;禁封中
// 不得拦截登录本身——登录后的落地页即禁封页)。
add_action('template_redirect', [$this, 'restrict_frontend_access'], 1);
// Enforce comment mute
add_filter('pre_comment_on_post', [$this, 'enforce_comment_mute']);
}
@@ -193,8 +195,8 @@ class Restriction {
/**
* 治理展示态(空串 = 无治理状态;ban 优先于 suspend,对齐后台单一状态语义)。
*
* 供资料卡提示框(Home::get_profile_notice)、用户中心页头、评论拒绝态、
* 昵称徽章共用,文案见 state_labels()。
* 供公开主页身份行徽章、用户中心元信息、评论拒绝态、昵称徽章共用,
* 徽章字段(文案/颜色/图标)见 state_badges()。
*/
public function get_display_state(int $user_id): string {
if ($user_id <= 0) {
@@ -213,60 +215,62 @@ class Restriction {
}
/**
* 治理展示态徽章文案 — 对齐参考主题公开主页禁封标识
* (zib_ban_author_header_identity_filter:封号 → 违规已封号,
* 小黑屋 → 小黑屋禁封中);禁言为 AKSRT 扩展状态。
* 治理展示态徽章字段(颜色 / 文案 / 图标统一管理,全前端提示面共用):
* 封号(ban) → c-red 违规已封号(最严重:无法登录);
* 小黑屋(suspend) → c-yellow 小黑屋禁封中(部分限制);
* 禁言(mute) → c-gray 禁言中(评论域限制)。
* 对齐参考主题 zib_ban_author_header_identity_filter 的红/默认双色语义,
* 禁言为 AKSRT 扩展状态。
*
* @return array<string,string> 展示态 => 文案
* @return array<string,array{label:string,tone:string,icon:string}> 展示态 => 徽章字段
*/
public static function state_labels(): array {
public static function state_badges(): array {
return [
self::STATE_BANNED => __('违规已封号', 'aksrt'),
self::STATE_RESTRICTED => __('小黑屋禁封中', 'aksrt'),
self::STATE_MUTED => __('禁言中', 'aksrt'),
self::STATE_BANNED => ['label' => __('违规已封号', 'aksrt'), 'tone' => 'c-red', 'icon' => 'ban'],
self::STATE_RESTRICTED => ['label' => __('小黑屋禁封中', 'aksrt'), 'tone' => 'c-yellow', 'icon' => 'ban'],
self::STATE_MUTED => ['label' => __('禁言中', 'aksrt'), 'tone' => 'c-gray', 'icon' => 'comment-slash'],
];
}
/**
* 治理展示态公开提示尾句(资料卡提示框,接在徽章文案之后):
* 封号 = 全面限制(无法登录、功能停用);
* 小黑屋 = 部分限制(发布、评论等操作受限);
* 禁言 = 仅评论域受限。
* 后台 user_report_ban_notice_text 配置后覆盖全部状态。
* 生效禁封信息行(查看禁封状态弹窗 / 申诉弹窗 / 登录后禁封页 / 管理端
* 禁封解封弹窗共用;对齐参考主题 zib_get_ban_info_modal 的字段集)。
*
* @return array<string,string> 展示态 => 提示文案
* @return array<string,mixed>|array{} 空数组表示无生效禁封/小黑屋。
*/
public static function state_notices(): array {
return [
self::STATE_BANNED => __('禁封期间无法登录,账号功能全面停用。', 'aksrt'),
self::STATE_RESTRICTED => __('发布、评论等操作已受限制。', 'aksrt'),
self::STATE_MUTED => __('暂时无法发表评论。', 'aksrt'),
];
}
/**
* 登录禁封提示文案(登录拦截 / 免密登录 / Toast 共用)。
* 对齐参考主题登录拦截页语义:封号 = 全面限制(无法登录),附期限与原因。
*/
public function login_ban_message(int $user_id): string {
$msg = __('您的账号已被封禁,禁封期间无法登录。', 'aksrt');
$ban = $this->get_active_ban($user_id);
if (is_array($ban)) {
$msg .= !empty($ban['expires_at'])
? ' ' . sprintf(__('解封时间:%s。', 'aksrt'), (string) $ban['expires_at'])
: ' ' . __('本次封禁为永久封禁。', 'aksrt');
$reason = (string) ($ban['reason'] ?? '');
$marker = "\n" . __('禁封说明:', 'aksrt');
$pos = strpos($reason, $marker);
if (false !== $pos) {
$reason = substr($reason, 0, $pos);
}
$reason = trim($reason);
if ($reason !== '') {
$msg .= ' ' . sprintf(__('原因:%s', 'aksrt'), $reason);
}
public function get_ban_info(int $user_id): array {
if ($user_id <= 0) {
return [];
}
return $msg;
$ban = $this->get_active_ban($user_id) ?: $this->get_active_suspend($user_id);
if (!is_array($ban)) {
return [];
}
$type = (string) ($ban['type'] ?? '');
$raw = (string) ($ban['reason'] ?? '');
$reason = $raw;
$desc = '';
$marker = "\n" . __('禁封说明:', 'aksrt');
$pos = strpos($raw, $marker);
if (false !== $pos) {
$reason = substr($raw, 0, $pos);
$desc = trim(substr($raw, $pos + strlen($marker)));
}
$no_appeal = get_user_meta($user_id, '_aksrt_no_appeal', true);
$no_appeal_desc = is_array($no_appeal) ? trim((string) ($no_appeal['desc'] ?? '')) : '';
return [
'type' => $type,
'status' => $type === self::TYPE_BAN ? __('禁封中(禁止登录)', 'aksrt') : __('小黑屋禁封中', 'aksrt'),
'start' => (string) ($ban['created_at'] ?? ''),
'end' => !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt'),
'reason' => trim($reason),
'desc' => $desc,
'no_appeal' => $no_appeal_desc !== '',
'no_appeal_desc' => $no_appeal_desc,
];
}
/**
@@ -305,16 +309,23 @@ class Restriction {
}
/**
* Enforce ban on login.
* 前台禁封拦截页(1:1 复刻参考主题 zib_user_ban_restrict_login /
* zib_die_page):封号(ban)用户登录后访问任何前台页面,均展示禁封状态页
* (红盒提示 + 禁封信息行 + 申诉权限 + 退出登录/申诉按钮)。
* 小黑屋/禁言为部分限制,可正常浏览站点,不走此拦截。
*/
public function enforce_login_ban($user, string $username, string $password) {
if (is_wp_error($user) || !$user instanceof \WP_User) {
return $user;
public function restrict_frontend_access(): void {
if (is_admin() || wp_doing_cron() || wp_doing_ajax()) {
return;
}
if ($this->is_banned($user->ID)) {
return new \WP_Error('user_banned', $this->login_ban_message($user->ID));
$user_id = get_current_user_id();
if ($user_id <= 0 || !$this->is_banned($user_id)) {
return;
}
return $user;
if (!function_exists('aksrt_render_user_ban_block_page')) {
require_once AKSRT_DIR . '/inc/helpers/user-ban.php';
}
aksrt_render_user_ban_block_page($user_id);
}
/**