A11:事件日志(环形缓冲 + 落盘)
事件从 evlog_put*() 进一个 256 条的 RAM 环(临界区里两条指令,**不阻塞、 不碰 flash**),由最低优先级的 t_log 攒够 16 条或最老那条等满 1 s 时批量写进 新的 `evlog` 分区(17 扇区 × 4 KB = **4335 条**)。A10 的事件出口原样接上, 判据那一层一个字没动。 八条实现约定(详见需求文档 §10.7): · 落盘不用 NVS 也不用 SPIFFS,是一块裸 data 分区 —— NVS 的页式 GC 会把 「写一条事件」变成一次不可预测的搬移。HAL 因此多出第六样 hal_store; · 17 个扇区不是 16:每扇区要拿一个 16 B 的槽当扇区头,4 KB 只装 255 条, 16 扇区 = 4080,差 16 条不到 §10.7 要求的 4096; · 序号 = flash 里的位置(epoch × 255 + 槽号),不是自增计数器 —— 重启接着数 不用存计数器;代价是没落盘的事件还没有序号,A12 读之前要先 flush; · 入队走关中断的临界区,**不是第三把锁**:入队方是最高优先级的 t_safety, 停在一把 t_log 持着的锁上等于让最低优先级的任务决定巡检节拍; · 丢事件可以,悄悄丢不行 —— 缓冲满了盖最旧的,但补一条 LOG_EV_LOST 记着丢了几条; · 每条记录带 CRC-8(见下),写失败就放弃该扇区剩余部分,被放弃的那段在序号 空间里是个洞,读回时给一条空记录占位(跳过的话上位机游标会卡住); · 每次上电记一条 LOG_EV_BOOT:时间戳上电从 0 计,没有它两次上电的记录会穿插。 事件类型的数字是 flash 格式的一部分,只许往末尾加;safety_ev_t → log_ev_t 是显式 switch 不是加偏移。§10.7 里还没人产生的六类号已占住(A12/A15/A16 去填)。 27 个用例;23 条注错逐个跑过,21 条变红。第一版校验和用异或,漏掉的正是最常见 的那条:payload 全 0 的记录被掉电切在第 8 个字节上时,后 8 个字节留在 0xFF, 而 8 个 0xFF 的异或恰好是 0 —— 半条的校验和与整条一模一样,扫描把半条当成 一条真事件。换成 CRC-8 之后拦住。另外两个只在这里才踩得到的坑:「掉电重启」 不能用 sim_hal_reset()(那是换新板子,flash 一起擦,测试永远绿),为此加了 sim_hal_reboot();「读到洞就跳过」这个错被测试里 static 数组的零初始化掩盖过 一次,缓冲要先填成非零。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
+106
-90
@@ -1,109 +1,125 @@
|
||||
# 交接
|
||||
更新:2026-09-07 完成任务包 **A10**(安全巡检的判据:SR-5 / SW-8)
|
||||
更新:2026-09-07 完成任务包 **A11**(事件日志:环形缓冲 + 落盘)
|
||||
|
||||
## 一句话现状
|
||||
`t_safety_step()` 在**放锁之后**跑两条判据,越界直接写引脚(不发消息给谁):
|
||||
SR-5「偏离设定 >±100 mV **连着两圈**」→ 先断 VBUS 再关 eFuse;
|
||||
SW-8「I < −50 mA 或电压被顶高 >+100 mV」→ **一圈看见就断 VBUS**(不碰 eFuse)。
|
||||
断电之前先 `t_actuator_abort()`(否则硬重启 4 s 后把电又抬回来),
|
||||
动作做完才记事件;事件走一个回调出去,A11 在那儿接环形缓冲。
|
||||
判据只在「该路该有电 + 这一圈的值新鲜 + 过了 60 ms 缓启动窗口」时武装。
|
||||
13 条 ctest(213 个用例)全绿,`idf.py build` 通过,**25 条注错跑过 24 条变红**。
|
||||
事件从 `evlog_put*()` 进一个 256 条的 RAM 环(临界区里两条指令,**不阻塞、不碰 flash**),
|
||||
由最低优先级的 `t_log` 攒够 16 条或最老那条等满 1 s 时批量写进 **`evlog` 分区**
|
||||
(17 个扇区 × 4 KB,**4335 条**)。序号 = 记录在 flash 里的位置(`epoch × 255 + 槽号`),
|
||||
所以掉电重启后接着数、`evlog_read(起始序号)` 直接给 A12 的 `events` 命令用。
|
||||
A10 的事件出口已经接上(`app_main.c` 里 `evlog_put_safety`),**判据那一层一个字没动**。
|
||||
14 条 ctest(240 个用例)全绿,`idf.py build` 通过,**23 条注错跑过 21 条变红**。
|
||||
|
||||
## 下一步
|
||||
**A11**:事件日志(环形缓冲 + 落盘)。验收是「写满 4096 条不丢最新;掉电重启后能读回」。
|
||||
A10 给它备好的:`safety_event_t`(§10.7 的单条形状,16 B)与
|
||||
`t_safety_set_event_sink(fn, ctx)` —— **只要把入队函数接上去就行,判据那一层不用动**。
|
||||
`app_main.c` 里现在接的是一个打日志的临时出口(`log_safety_event()`),换掉它。
|
||||
⚠️ 回调是在断电之后、不持任何锁的情况下被调的,入队函数不许阻塞。
|
||||
**A12**:命令处理器(语义 + 序号判重 + 限幅)。验收是「同序号发两遍只执行一次;
|
||||
越界命令被拒且记事件」。A11 给它备好的:
|
||||
- `evlog_read(from_seq, out, max)` 就是 `events 起始序号` 那条命令的实现
|
||||
—— ⚠️ **先 `evlog_flush(0)` 再读**,没落盘的事件还没有序号(见下面「几条约定」)。
|
||||
- 「越界命令被拒」记 `LOG_EV_CMD_REJECT`(号已经占住,`arg` 放命令号)。
|
||||
- `evlog_stats()` 里的 `oldest_seq` / `next_seq` 是上位机的游标两端,
|
||||
`dropped` / `io_errors` 该进 `status` 的回复里。
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `main/t_safety.h` / `t_safety.c` — 两条判据 + 事件(约 200 行,一多半是注释)
|
||||
· 阈值都带出处:`T_SAFETY_DEV_MV` 100(SR-5)、`T_SAFETY_DEV_HOLD_CYCLES` **2**
|
||||
与折算值 `_HOLD_MS` 20、`T_SAFETY_REV_UA` −50000(设计文档 §15.2 对策 2)、
|
||||
`T_SAFETY_REV_BUDGET_MS` 200(SW-8)、`T_SAFETY_SETTLE_MS` 60(tON+斜坡 17 ms 的 3 倍)
|
||||
· `safety_ev_t` 四种事件 + `safety_event_t`(§10.7 的单条形状)+ 事件出口回调
|
||||
· 每路判据状态 `safety_rail_t` 放在快照里,`t_safety_init()` 一并清干净
|
||||
- `main/rail_cfg.h` / `.c` — 新增 `rail_efuse_is_on()` / `rail_vbus_is_on()`
|
||||
(两根 EN 线的影子,**只在写成功时更新**)
|
||||
- `main/rail.h` / `.c` — 新增 `rail_setpoint_mv()` / `rail_setpoint_reset()`
|
||||
(五路设定值的影子,初值就是 DAC 的 NVM 默认 3900)
|
||||
- `main/app_main.c` — `log_safety_event()` 临时事件出口;`t_safety_set_event_sink()`
|
||||
接在建任务之前
|
||||
- `test/test_t_safety_rules.c` — 29 个用例(新文件;A8 那份 `test_t_safety.c` 没动)
|
||||
- `main/phonerail.h` — 版本 `0.1.0-a10`
|
||||
- `../需求文档.md` §10.2 — 记 A10 落地 + 七条实现约定
|
||||
- `main/evlog.h` / `evlog.c` — 环形缓冲 + 落盘 + 按序号读回(约 500 行,一半是注释)
|
||||
· `log_ev_t` 十四种事件(**数字就是 flash 格式**,只许往末尾加)
|
||||
· `evlog_rec_t` 16 B(§10.7 的「单条」)+ 每条一个 **CRC-8**
|
||||
· `evlog_init/put/put_safety/due/flush/read/stats/capacity`
|
||||
- `components/phonerail_hal/include/hal_store.h` + `sim/hal_store_sim.c` +
|
||||
`esp/hal_store_esp.c` — **HAL 的第六样**:掉电不丢的一块字节区(擦扇区 / 写字节 / 读)。
|
||||
sim 那份按 **NOR 的规矩**来:写是按位与、擦是整扇区、可以被「掉电」切成半条。
|
||||
- `components/phonerail_hal/include/hal_lock.h` + 两份实现 — 新增
|
||||
`hal_crit_enter/exit()`。**这不是第三把锁**,是关中断的临界区(ESP 侧是 portMUX,
|
||||
双核上两个核同时入队是真会发生的)。sim 侧会数「在临界区里干了会阻塞的事」的次数。
|
||||
- `sim/sim_hal.{h,c}` — 新增 **`sim_hal_reboot()`**(掉电重启:除 flash 内容外全清)
|
||||
与假 flash 的控制面(`sim_store_tear_next_write()` 等)
|
||||
- `main/app_main.c` — `evlog_init()`(失败不 abort)、事件出口接 `evlog_put_safety`、
|
||||
`task_log` 填上(100 ms 醒一次、`evlog_due()` 说要写才写、丢过事件就告警)
|
||||
- `partitions.csv`(新)+ `sdkconfig.defaults` — 自定义分区表,多一块 `evlog` 68 KB
|
||||
- `test/test_evlog.c` — 27 个用例(新文件)
|
||||
- `main/phonerail.h` — 版本 `0.1.0-a11`
|
||||
- `../需求文档.md` §10.7 — 记 A11 落地 + 八条实现约定
|
||||
|
||||
## 验收命令与结果
|
||||
```
|
||||
$ bash host/test.sh
|
||||
100% tests passed, 0 tests failed out of 13 # +test_t_safety_rules(29),共 213 例
|
||||
100% tests passed, 0 tests failed out of 14 # +test_evlog(27),共 240 例
|
||||
|
||||
$ .\build_idf.ps1 # PowerShell
|
||||
phonerail.bin binary size 0x3d650 bytes. ... 0xc29b0 bytes (76%) free.
|
||||
phonerail.bin binary size 0x3ec30 bytes. ... 0xc13d0 bytes (75%) free.
|
||||
Project build complete.
|
||||
|
||||
# 25 个「一定会被写出来的错」逐个注进去跑过(一次性脚本,没入库):
|
||||
# SR-5: 只判过压方向 / 阈值写成 >= / 看见一圈就关 / 回到范围内不清零 /
|
||||
# **确认窗口改回按墙钟算 >= 20 ms** / 设定值写死 3900
|
||||
# 闸门: 盲跑那一路照样判 / 关着的路照样判 / 没有缓启动窗口 / 跳过的那一圈也判
|
||||
# 关路: 不 abort / 只关 eFuse 不断 VBUS / 顺序反过来 / 事件记在断电之前
|
||||
# SW-8: 符号写反 / 不看 VBUS 通没通 / 顺手把 eFuse 也关了 /
|
||||
# 不打断 EDL / 无条件打断这一路的序列 / 挪到 SR-5 后面
|
||||
# 其它: 判据跑在放锁之前 / 顺手多读一次寄存器 / 事件里记设定值 / …
|
||||
# → 24 条变红。剩下 1 条见下面「抓不到的那一条」。
|
||||
$ python .../gen_esp32part.py build/partition_table/partition-table.bin
|
||||
evlog,data,undefined,0x110000,68K, # 17 个扇区
|
||||
|
||||
# 23 个「一定会被写出来的错」逐个注进去跑过(一次性脚本,没入库):
|
||||
# 环形: 满了丢最新而不是盖最旧 / 丢了不补 LOST / 补记 LOST 之后不清零 /
|
||||
# 攒批计时从最新一条算起
|
||||
# 校验: 退回异或和 / 记录从扇区头那一槽开始写 / 假 flash 写成覆盖而不是按位与
|
||||
# 落盘: 换扇区时不擦 / 绕圈后不更新 oldest_seq / 写失败后不放弃这个扇区 /
|
||||
# 落盘时不补记 LOST / flush 不守预算 / 攒批没有超时兜底 / 攒够一批也不写
|
||||
# 重启: 每次上电都重新格式化 / 扫描取 epoch 最小的扇区 / 扫描停在上一条
|
||||
# 读回: 读到洞就跳过 / 读不 clamp 到 oldest_seq
|
||||
# 其它: 事件时间戳改成记账时刻 / 映射写成加个偏移
|
||||
# → 21 条变红。剩下 2 条见下面「抓不到的那两条」。
|
||||
```
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- ⚠️⚠️ **「持续 20 ms」按墙钟算是错的,必须数圈。** 第一版写的是
|
||||
`now − 第一次看见 >= 20 ms`,测试一半红一半绿,看上去像是测试写飘了。
|
||||
真因:巡检的采样时刻带**亚毫秒残差** —— 一圈自己占 1.56 ms 总线,而
|
||||
`sched_periodic_wait()` 是按整毫秒睡的,于是相邻两次判据的时刻是
|
||||
**21 / 19 ms 交替**(实测 1, 22, 41, 62, 81, 102, 121, 142 …)。
|
||||
19 ms 那一次不满足 `>= 20`,判据要拖到第三圈 = **40 ms**,超了 §12.1 的验收,
|
||||
而且是**间歇性**的。改成「连着两圈都越界」之后一次都不飘。
|
||||
**凡是「持续 N 个周期」的判据都数圈,别拿墙钟去凑。**
|
||||
回归拦网是 `test_the_cut_is_never_late_because_of_sub_millisecond_jitter()`,
|
||||
它跑 20 轮、两种相位都踩得到。
|
||||
- ⚠️ **「盲跑不判据」这条,测试的写法决定了它能不能被抓住。** 第一版是
|
||||
「改坏寄存器 → 立刻让器件失联 → 跑 1 秒」,而那时快照里躺着的是**正常值**,
|
||||
于是「判据不看 `rail_ok`」这个错一点痕迹都不留,测试永远绿。
|
||||
正确写法是**先让坏值真的读进快照一次**,再让器件失联。
|
||||
凡是测「陈旧值不许被采信」的,都要先把陈旧值做成一个**越界**的值。
|
||||
- **判据顺序:SW-8 在前,SR-5 在后。** 过压且 VBUS 通着时两条都成立,
|
||||
先撤掉能量来源(VBUS),20 ms 后再由 SR-5 决定要不要连这一路一起关掉。
|
||||
反过来的话,第一圈就把电关了,日志里只剩一条「过压关路」,
|
||||
「是 USB 灌回来的」这个信息丢了。
|
||||
- **`t_actuator_abort()` 要在断电之前**,虽然它最坏会等 595 那把锁 20 ms
|
||||
(`ACT_LOCK_TIMEOUT_MS`)、让巡检丢一拍。反过来(先断电再 abort)有个真实的洞:
|
||||
abort 阻塞在锁上的那 20 ms 里,优先级更低的 t_actuator 反而能跑,
|
||||
`HR_OFF` 到点就把 `EFUSE_EN` 抬回去了。**丢一拍认了** —— SR-5 原文写着它
|
||||
「仅承担记录与告警,不承担实时保护」。
|
||||
- **两个影子都只在写成功时更新**(`rail_efuse_is_on` / `rail_setpoint_mv`)。
|
||||
方向是有讲究的:关的那次写失败 ⇒ 影子留在 on ⇒ 判据继续武装(还会再关一次);
|
||||
开的那次写失败 ⇒ 影子留在 off ⇒ 不会对着一路没开成的电报偏差。两边都倒向安全侧。
|
||||
- **判据一条 I²C 事务都不许发**,所以设定值、两根 EN、正在跑的序列全部走影子。
|
||||
多读一次就把 §10.3 那 7.8 % 的预算顶穿,A8 的
|
||||
`test_one_cycle_is_exactly_thirteen_transactions()` 会当场变红。
|
||||
- **抓不到的那一条**:「关路之后把 `dev_n` 清零」注进去仍然全绿 ——
|
||||
它只在**写引脚失败**时才有区别(影子留在 on ⇒ 下一圈立刻再关一次,
|
||||
清零的话要再等两圈),而 sim 里造不出「已配置的输出脚写失败」。
|
||||
所以那一行干脆没写(不写才是 do-nothing 的那一边),理由留在代码注释里。
|
||||
- ⚠️⚠️ **校验和用异或是错的,必须用 CRC。** 掉电把一次写切在第 8 个字节上时,
|
||||
后 8 个字节留在 0xFF —— 而 **8 个 0xFF 的异或恰好是 0**。于是一条
|
||||
`bus_mv`/`current_ua` 都是 0 的记录(`LOG_EV_BOOT` 就长这样),
|
||||
**半条的异或和与整条一模一样**,扫描把半条当成一条真事件,接着往后写。
|
||||
这条错在正常运行里一点痕迹都没有,只在掉电之后出现。
|
||||
拦网是 `test_a_torn_write_with_zero_payload_is_still_rejected()`。
|
||||
- ⚠️ **「掉电重启」的测试不能用 `sim_hal_reset()`**:那是「换了一块新板子」,
|
||||
flash 一起擦掉,于是测的是「刚擦干净的 flash 读不出东西」—— 永远绿。
|
||||
为此专门加了 `sim_hal_reboot()`(CPU 侧全清、flash 一个字节不动)。
|
||||
凡是测「掉电后还在不在」的,先问一句:这个假实现里,什么东西该活下来。
|
||||
- ⚠️ **「读到洞就跳过」这个错被 `static` 数组的零初始化掩盖过一次。**
|
||||
`evlog_read()` 遇到坏槽要放一条空记录**占住位置**,跳过的话上位机游标就对不上;
|
||||
但测试用的 `static evlog_rec_t buf[]` 本来就是全 0,而「空记录」也是全 0,
|
||||
于是注错之后测试照样绿。**读之前先把缓冲填成非零**(现在填 0xAA)。
|
||||
- **攒批的计时起点是环里最老那一条,不是最新那一条。** 取最新的话,事件慢慢地
|
||||
一条一条来(真板上就是这个样子)时窗口每次被重置,**永远不 due** ——
|
||||
现象是「日志一直不落盘」,而每一条断言都是绿的。
|
||||
拦网是那条「一条 + 600 ms + 一条 + 600 ms」的用例。
|
||||
- **扫描要取 epoch 最大的扇区**,而这件事在「只写过一个扇区」的用例里
|
||||
一点区别都没有(min == max)。测重启必须先写**超过 255 条**跨过扇区边界,
|
||||
否则整段绕圈代码一行都没跑到。
|
||||
- **NOR 的写是按位与,不是赋值。** 假 flash 写成 `=` 的话,「忘了先擦」
|
||||
这个错在宿主机上一次也复现不出来,而真板上的现象是「记录读出来是两条的与」。
|
||||
- **`t_safety` 的事件出口是在断电之后、不持任何锁时被调的**,所以入队里
|
||||
`ESP_LOGW` 那一行必须排在 `evlog_put_safety()` **后面** —— 抢串口是会等的。
|
||||
- **抓不到的那两条**(注进去仍然全绿,理由留在代码注释里):
|
||||
① `rec_valid()` 里的「全 0xFF 直接判空」:当前这个 CRC 的初值/多项式下,
|
||||
擦干净的槽本来就校验不过,那一行是冗余的一道保险 —— 它只在**有人改了
|
||||
CRC 的多项式或初值**时才有区别,而那种改动没有任何测试拦得住。
|
||||
② `hdr_valid()` 里的 `epoch % 扇区数 == 扇区号`:它保的是「扇区号由 epoch
|
||||
算出来」这条被后面所有代码假定的不变量,但要造出反例得让分区几何
|
||||
在两次上电之间变掉,而 sim 的几何是编译期常量。
|
||||
两条都留着 —— 它们保的是别人改代码时的假设,不是当前这一版的行为。
|
||||
|
||||
## 这一层的几条约定(写 A11/A12/A15 之前看一眼)
|
||||
- **A11 只接 `t_safety_set_event_sink()`,不要动判据。** 判据那一层已经保证
|
||||
「动作在前、记录在后」,入队函数只管入队;阻塞或回头调 `t_safety_*` 都是错的。
|
||||
§10.7 里其它几类必记事件(eFuse FAULT / PG 变化 / 过温降载 / 越界命令被拒 /
|
||||
I²C 器件失联)各归各的包,A10 只产生自己这两条判据的四种。
|
||||
- **A12 的命令处理器改电压时走 `rail_set_mv()`,别绕过它。** 判据比的是那里的影子;
|
||||
绕过去直接写 DAC 的话,改完那一路会在 20 ms 后被自己的判据关掉。
|
||||
- **A15 的启动顺序里,五路上电要错开 100 ms(§10.9 第 5 步)——
|
||||
这和 60 ms 的缓启动窗口是相容的**:每一路的窗口从它自己的 `EFUSE_EN↑` 算起。
|
||||
另外 `t_actuator_init()` 必须排在巡检任务之前(判据要 abort 它),
|
||||
`app_main.c` 里已经是这个顺序,别调过来。
|
||||
- **改了任何一个阈值就是改了需求**,`test_thresholds_match_the_documents()` 会拦;
|
||||
先去改 SR-5 / SW-8 / 设计文档 §15.2,再改这里。
|
||||
- ⚠️ **`T_SAFETY_DEV_HOLD_MS` 和 `_HOLD_CYCLES` 是同一件事的两种写法**
|
||||
(毫秒 = (圈数−1) × 巡检周期),代码用圈数、文档用毫秒,测试拿任务表里的周期
|
||||
把两者钉在一起。巡检周期哪天改了,那条断言会当场对不上 ——
|
||||
这正是它存在的理由,别去「修」它。
|
||||
## 这一层的几条约定(写 A12/A15/A16 之前看一眼)
|
||||
- **要读最新的事件,先 `evlog_flush(0)`。** 序号是 flash 里的位置算出来的,
|
||||
所以还在 RAM 里的事件**还没有序号**,`evlog_read()` 看不见它们。
|
||||
这不是疏漏:入队时发号会在缓冲溢出时留下永远补不上的空号。
|
||||
- **产生事件的人只调 `evlog_put()`,不碰 flash、不调 `evlog_flush()`。**
|
||||
`t_log` 是唯一碰 flash 的任务。在 t_safety / t_actuator 里 flush,
|
||||
等于把一次几毫秒的 flash 写事务塞进 20 ms 的节拍里。
|
||||
- **新事件类型只能往 `log_ev_t` 末尾加。** 中间插一个,flash 里跑了半年的
|
||||
历史记录会整体挪位,**而且读出来仍然是合法记录**,没有一处会报错。
|
||||
`test_flash_format_is_frozen()` 把每一个数字都钉死了 —— 那条用例变红时,
|
||||
要改的是你的枚举,不是那条用例。
|
||||
- **§10.7 的必记事件里还有六类没人产生**,号已经占好:`LOG_EV_EFUSE_FAULT` /
|
||||
`PG_CHANGE` / `I2C_LOST`(A15 的降级矩阵)、`THERMAL_DERATE` / `FAN_STALL`(A16)、
|
||||
`REBOOT` / `CMD_REJECT`(A9 的序列 + A12 的命令)。填的时候顺手把
|
||||
`evlog_ev_str()` 的名字和 `test_every_event_type_has_a_name()` 一起看一眼。
|
||||
- **改了 `partitions.csv` 里 `evlog` 的大小,要跟着改 `sim_hal.h` 的
|
||||
`SIM_STORE_SECTORS`。** 两边不一致时宿主机测试仍然全绿,板子上却是另一个容量。
|
||||
`test_capacity_meets_the_requirement()` 把它们钉在一起了,但它钉的是 sim 那一侧
|
||||
—— csv 那一侧只有人眼。
|
||||
- **`hal_crit_enter/exit()` 不是第三把锁**(§10.3 的共享资源只有两个)。
|
||||
它是关中断的临界区:不阻塞、没有持有者、不参与「不许嵌套两把锁」那条规则。
|
||||
里面**只许改内存** —— sim 的 `sim_crit_blocking()` 会数「在临界区里等锁或睡」
|
||||
的次数,`test_evlog.c` 的 `tearDown()` 每个用例都查一次,必须恒为 0。
|
||||
- **`evlog_init()` 失败不许 abort。** 日志是排查手段,不是安全链的一环(§10.2):
|
||||
没有分区时事件照进 RAM,只是落不了盘,五路照供电。这与 `selftest_or_die()`
|
||||
的区别在于:那里错的是「命令会发到哪一路」,这里错的只是「记不下来」。
|
||||
|
||||
在新工单中引用
屏蔽一个用户