Java人脸识别Demo实战:从AccessToken到人脸检测与比对
简介这是一份基于商汤科技人脸识别能力的安卓示例工程面向希望快速集成人脸检测、关键点定位、身份比对等能力的 Java 开发者。项目以三十个 Java 源码文件为核心配合十六个 XML 界面与资源文件外加 Gradle 构建脚本、SO 原生库、JAR 依赖、模型文件及属性配置等共七十一个文件压缩包约二十六点八二 MB目录结构完整可直接导入安卓开发环境进行学习调试。已有七百九十八人学习下载适合初、中级开发者拆解参考通过阅读工程内的人脸识别调用链、模型加载逻辑与界面交互代码可以直观理解商汤 SDK 的接入步骤、回调处理与异常规避方式从摄像头画面采集、人脸框选标定到比对结果展示形成完整闭环减少自行摸索的时间成本更好地迁移到自身业务场景中。1. 这份Java人脸识别demo先把它的边界说清楚拿到一份名为SencetimeFaceDemo化名的资源包第一反应别急着找main方法。它是一个非常典型的“调第三方AI开放平台人脸API”的Java示例工程核心不是训练模型而是把“申请凭证→人脸检测→人脸比对”这条链路用Java代码串起来。说人话就是你用某AI厂商的人脸识别能力这份demo把调用姿势和边界条件都摆给你看了。适合两种人一种是想在半小时内把人脸检测、人脸比对跑通并接进自己系统的后端开发另一种是课程设计或毕设里要快速出效果的Java方向学生。需要注意它不是本地推理的离线人脸识别库而是联网调用服务的客户端示例所有能力都建立在HTTP请求和Token鉴权之上。2. 凭证与调用链先把AccessToken拿到手再谈人脸检测2.1 为什么先拿AccessToken再写业务代码所有AI开放平台的人脸接口都采用类似的鉴权模型调用方先用自己的应用凭证换取一个短期有效的访问令牌后续每次人脸检测、对比请求都要在请求头里带上这个令牌。换成Java代码去理解就是先发一次POST请求拿token再带着token继续干活。这种设计对人脸识别场景是有实际意义的。人脸检测的图片请求体积比较大如果每个请求都带上AppKey和AppSecret并做二次签名校验网关层的开销会明显升高而Token是一次性校验完成后下发的临时通行证之后网关只做轻量校验。所以你的Java工程里第一段代码应该是TokenClient而不是人脸检测代码。我一般建议把凭证信息放在环境变量或单独的配置类里而不是写死在代码中。原因很实际demo里写死方便但一旦部署到测试环境或生产环境换一组凭证就要改代码重新打包太折腾了。资源包里的主类结构也符合这个思路配置与调用逻辑是分离的。2.2 用Java写一个最小调用栈OkHttp封装与连接复用常见的做法是用OkHttp作为HTTP客户端配合Jackson解析返回JSON。下面这段就是获取AccessToken的最小实现import okhttp3.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class TokenClient { private static final String TOKEN_URL https://your-gateway/oauth2/token; private static final String APP_KEY System.getenv(FACE_APP_KEY); private static final String APP_SECRET System.getenv(FACE_APP_SECRET); private final OkHttpClient client new OkHttpClient(); private final ObjectMapper mapper new ObjectMapper(); public String getAccessToken() throws Exception { FormBody formBody new FormBody.Builder() .add(grant_type, client_credentials) .add(app_key, APP_KEY) .add(app_secret, APP_SECRET) .build(); Request request new Request.Builder() .url(TOKEN_URL) .post(formBody) .build(); try (Response response client.newCall(request).execute()) { String respBody response.body().string(); if (!response.isSuccessful()) { throw new IllegalStateException(token请求失败: response.code() respBody); } JsonNode node mapper.readTree(respBody); return node.get(access_token).asText(); } } }逻辑说明grant_type固定为client_credentials表示当前是“应用凭证直接换Token”的模式AppKey和AppSecret通过环境变量注入避免硬编码OkHttp的newCall().execute()是同步调用在业务Demo里够用。有一点值得注意AppSecret不能出现在URL参数或日志里否则抓包就能看到你的完整凭证。返回的JSON里access_token字段就是后面所有接口要用到的令牌。参数层面最常踩的问题是表单字段名写错。有的平台用client_id和client_secret有的用app_key和app_secret拿到demo先看清它用的哪套。这个资源包里TokenClient用的是app_key/app_secret风格如果你要接的是别的厂商开放平台字段名大概率要改。2.3 解析返回值先读懂JSON结构再写业务判断拿到Token后业务代码要从响应JSON中提取人脸数据。人脸检测接口的返回结构一般长这样注意层级嵌套{ error_code: 0, error_msg: SUCCESS, result: { face_num: 1, face_list: [ { face_token: 一串唯一的人脸标识, location: {left: 120, top: 80, width: 150, height: 180}, face_probability: 0.99 } ] } }写Java解析时我习惯用JsonNode一层层取而不是直接映射成POJO。原因有两个第一人脸检测的返回字段在不同厂商之间差异很大映射成POJO容易因为字段缺失直接抛异常第二demo阶段高频改动字段用JsonNode取字段改起来最省事。取face_token的代码路径是result.face_list[0].face_token取位置坐标是result.face_list[0].location.left。这个结构在资源包的示例代码里有标注新手容易看错层级把result漏掉。字段路径含义典型值error_code接口调用是否成功0为成功result.face_num识别到的人脸数量1result.face_list[0].face_token当前人脸唯一标识32位字符串result.face_list[0].location人脸在图片中的位置左上角x/y、宽高这里最容易被忽略的是error_code和HTTP状态码是两套东西。HTTP 200不代表识别成功业务错误码才是真正的判断依据。你如果写代码时只看response.isSuccessful()等于漏掉了识别失败的分支。3. 写代码前先避坑Token、Base64与坐标系的五个边界3.1 AccessToken的过期时间比你想象得早现象上午调试的时候一切正常下午重新启动程序第一次人脸检测请求就返回了鉴权失败。原因Token不是永久有效的这个资源包里对应的开放平台默认有效期是数天而且很多新手误以为“每次启动拉一次就够”没有考虑到进程启动后长时间运行的情况。解决不要每次都重新拉Token也不要只拉一次而是做一个带过期时间的缓存。我一般会在内存里放一个TokenHolder记录token和获取时间每次调用前检查是否到了过期阈值的80%快到期就提前刷新。代码上就是加一个定时任务每几个小时重新拉一次Token并更新内存值。3.2 Base64图片体积膨胀反而拖垮上传现象本地一张JPG图片只有几百KB转成Base64之后请求却超时了。原因Base64编码会让体积增加约33%再加上JSON转义和请求头开销实际传输体积可能接近原始图片的1.5倍。人脸检测接口一般对图片大小有明确上限超了直接报参数错误。解决上传前先压缩。常见做法是在Java端用ImageIO读取图片并重新缩放统一压到最长边不超过某个像素值然后以JPEG格式输出质量参数设为80到85之间。压缩之后再转Base64体积通常能降到原来的三分之一以下。BufferedImage original ImageIO.read(new File(test.jpg)); int width original.getWidth(); int height original.getHeight(); int maxSide 800; double scale Math.min(1.0, maxSide * 1.0 / Math.max(width, height)); int newWidth (int) (width * scale); int newHeight (int) (height * scale); BufferedImage scaled new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); scaled.getGraphics().drawImage(original, 0, 0, newWidth, newHeight, null); ByteArrayOutputStream baos new ByteArrayOutputStream(); ImageIO.write(scaled, jpg, baos); byte[] imageBytes baos.toByteArray(); String base64 Base64.getEncoder().encodeToString(imageBytes);逻辑说明scale先取最长边与目标尺寸的比值避免放大图片TYPE_INT_RGB是为了去掉可能存在的Alpha通道干扰识别效果压缩后用JPEG格式输出体积比PNG小得多。这里有个血泪经验不要在循环里反复压缩同一张图缓存压缩结果会比每次调用都重新加载图片快很多。3.3 坐标系的坑center、left和缩放比现象用接口返回的坐标在图片上画人脸框框的位置总是偏右下方尤其对宽图偏差特别明显。原因不同接口对人脸位置的定义不一样。有的返回左上角坐标加宽高有的返回中心点坐标加宽高你如果直接拿中心点当左上角用画出来的框当然不对。解决拿到location先打印原始数据确认字段名到底是left/top还是center_x/center_y。资源包示例里用的是left和top直接画框没问题如果你换成别的厂商接口一定要先做一次坐标打印。另外如果你在页面端展示图片时对图片做了缩放Java端拿到的像素坐标必须乘上缩放比才能正确对齐。3.4 QPS限流并发一高就报错现象写了个for循环批量检测几十张图片跑到中间突然连续失败错误码变成请求太频繁。原因AI开放平台的人脸接口默认有QPS限制免费额度下通常是单实例每秒几次你一次性并发发过去必然触发限流。解决在循环里加间隔或者用信号量控制并发数。最简单粗暴的方法每次请求后Thread.sleep(100)到200毫秒把速率压到每秒5次以下。如果追求效率可以对着错误响应做指数退避重试第一次失败等500毫秒再失败等1秒最多重试三次。3.5 本地图片名称带中文导致上传失败现象图片路径是“自拍_正面.jpg”程序报找不到文件但路径明明存在。原因Windows下Java的File类对中文路径依赖默认字符集某些环境配置下解析会乱码。解决不要依赖new File(路径)直接读取而是用Files.newInputStream(Paths.get(path))并确保启动参数里加了-Dfile.encodingUTF-8。这类问题不出在线程不安全的并发上而是出在环境字符集不一致上排查时容易忽视。4. 核心功能落地人脸检测、特征提取与置信度比对4.1 人脸检测拿到face_token才能继续资源包里最核心的调用逻辑就是人脸检测。它的目标不是告诉你图片里有没有人而是给每个人脸生成一个唯一的人脸标识face_token。后续的人脸比对、人脸库管理都依赖这个token。没有token比对就无从谈起。import okhttp3.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class FaceDetectClient { private final String token; private final OkHttpClient client new OkHttpClient(); public FaceDetectClient(String token) { this.token token; } public String detectAndGetFaceToken(String base64Image) throws Exception { JSONObject params new JSONObject(); params.put(image_type, BASE64); params.put(image, base64Image); params.put(face_field, age,gender,face_token); params.put(face_probability, 0.7); RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), params.toString() ); Request request new Request.Builder() .url(https://your-gateway/face/v3/detect) .addHeader(Authorization, Bearer token) .post(body) .build(); try (Response response client.newCall(request).execute()) { String respBody response.body().string(); JsonNode root new ObjectMapper().readTree(respBody); if (root.get(error_code).asInt() ! 0) { throw new IllegalStateException(root.get(error_msg).asText()); } return root.get(result).get(face_list).get(0).get(face_token).asText(); } } }逻辑说明image_type字段必须和image字段对应传BASE64时image里放的就是Base64字符串face_field用来声明你需要返回哪些附加属性这里我只取了年龄、性别和face_token少取字段能减少响应体大小。face_probability是对人脸检测置信度的过滤阈值低于0.7的检测结果会被过滤掉。这个值在实际业务中需要调证件照场景可以放到0.95以上监控抓拍场景往往需要降到0.5以下才不会漏检。参数层面有两个注意点第一base64Image不能带前缀“data:image/jpeg;base64,”有些前端传图会带上Java端要先把前缀去掉再传第二face_list是按检测置信度排序的list的第一个元素通常是画面里最清晰的人脸如果你要处理多人图片得遍历整个列表而不是只取第一个。4.2 人脸比对置信度阈值怎么定人脸比对的输入是人脸特征表述的组合。在商汤这类开放平台上就是两个face_token做比对返回一个表示相似度的分值。代码结构如下public class FaceCompareClient { private final String token; public FaceCompareClient(String token) { this.token token; } public double compare(String faceToken1, String faceToken2) throws Exception { JSONObject params new JSONObject(); params.put(face_token1, faceToken1); params.put(face_token2, faceToken2); RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), params.toString() ); Request request new Request.Builder() .url(https://your-gateway/face/v3/compare) .addHeader(Authorization, Bearer token) .post(body) .build(); try (Response response client.newCall(request).execute()) { String respBody response.body().string(); JsonNode root new ObjectMapper().readTree(respBody); return root.get(result).get(score).asDouble(); } } }这里的score就是相似度取值范围一般在0到100之间。阈值怎么定我一般分三档大于80的同人60到80的待人工确认低于60的直接判不同人。注意这个阈值不是拍脑袋定的受光照、年龄、遮挡影响很大。同一张身份证照片和现场抓拍照比对分数可能只有70多这种场景下不宜把阈值设太高。资源包示例里默认给的阈值是80闸机场景通常要压低到75左右。你在校准阈值的时候最好准备几十张正样本同一人不同角度和负样本不同人画一条ROC曲线再定。4.3 活体检测demo之外的补全很多demo到这里就结束了但真实业务场景大多需要加上活体检测否则一张带人脸的照片就能轻松骗过比对。资源包因为没有把活体检测放到主流程里需要你自行补上。平台的静默活体接口一般接收一张图片返回该人脸是否为真实本人而非纸张或屏幕翻拍。整体思路是先做一次检测拿到face_token再调用活体接口判断是否为活体两项都通过后才允许执行比对。顺序不能反因为活体接口需要基于人脸位置做分析没有face_token就无法定位。JSONObject params new JSONObject(); params.put(face_token, faceToken); params.put(face_type, LIVE); params.put(liveness_type, SILENT);liveness_type这里用SILENT代表静默活体适合闸机或门禁场景如果还需要用户配合做指定动作就要换成动作活体模式响应时间和交互流程都会更长。存活体的阈值同样有讲究demo里给的是能过就过的参数实战中需要结合防攻击等级调整。5. 从Demo到可复用批量任务与本地服务化改造拿到手的demo如果只是跑通一次检测就完事价值打折。我一般会把它往两个方向延伸一是批量处理能力二是服务化封装。批量任务的核心是控制节奏。假设你要注册一个1000人的本地人脸库不能一次性循环提交。我会在循环里加一个均匀间隔并且维护一个成功/失败计数器。失败超过阈值就暂停整个队列打印出当前失败的文件路径便于事后补录。更重要的是批量任务要把文件路径、face_token和注册时间归档到一张表里这样后续比对不用频繁调用检测接口直接拿本地保存的face_token做比对成本低很多很多。服务化改造是把两个Client塞进一个Spring Boot工程里。常见的做法是给TokenClient配一个定时刷新Job每2小时检查一次内存中的Token剩余有效期小于1小时就重新获取人脸检测和比对逻辑则封装成Service接口用线程池接收外部请求。线程池的核心参数可控避免外部批量请求直接把QPS打爆。再加一个入参校验层Base64字符串长度超过阈值就直接拒绝并返回提示而不是等到网关层超时。验证方法也有一个固定套路拿你自己的正脸照片A检测得到token1再拍一张光线稍差的正面照B检测得到token2调Compare接口预期score大于80再用同事或家人的照片做负样本预期score低于60。如果负样本分数也高于80先检查是不是照片被压缩过度导致人脸特征丢失其次检查是否Base64传输过程中图片被损坏。我每次改完代码都会强制走一遍正向和反向验证这个习惯救过我很多次。希望你也能用得上这套链路少走弯路。本文还有配套的精品资源点击获取

相关新闻

【DUK7325T】复旦微 JFM7K325T 国产 Kintex-7 FPGA 开发板硬件说明——工业级 -40~85℃,Vivado 兼容,XC7K325T 国产替代首选

【DUK7325T】复旦微 JFM7K325T 国产 Kintex-7 FPGA 开发板硬件说明——工业级 -40~85℃,Vivado 兼容,XC7K325T 国产替代首选

【DUK7325T】复旦微 JFM7K325T 国产 Kintex-7 FPGA 开发板硬件说明——工业级 -40~85℃,Vivado 兼容,XC7K325T 国产替代首选产品型号:DUK7325T V1.0 | 品牌:笃远电子(上海笃远电子有限公司)&…

2026/10/11 19:23:25 阅读更多 →
AI自动生成断言:破解测试预言难题的实战指南

AI自动生成断言:破解测试预言难题的实战指南

1. 测试预言:测试里最被低估的瓶颈前阵子团队做接口自动化回归,用例覆盖率提上去了,但真正开始跑的时候发现问题全出在“断言”上。测试同学花大力气构造了请求参数、搭好了执行环境,结果卡在最后一步:到底该断言什么&…

2026/10/11 19:22:25 阅读更多 →
隧道智能照明:如何实现按需调光、节能又安全

隧道智能照明:如何实现按需调光、节能又安全

一、行业背景:隧道照明的两难问题隧道作为交通基础设施,照明系统承担着保障行车安全的核心职能,同时也面临突出的能耗问题。隧道通常需要长时间、大范围照明,若长期满负荷恒亮运行,电能消耗十分可观;而照明…

2026/10/11 19:22:25 阅读更多 →

最新新闻

Python汽车销售数据可视化与预测实战:从Excel到可落地分析链路

Python汽车销售数据可视化与预测实战:从Excel到可落地分析链路

简介:这份资源面向具备一定Python基础、希望入门数据分析与时间序列预测的学习者,围绕汽车销售场景提供一套完整的数据可视化与销量预测实战方案。内容涵盖数据获取与清洗、销量波动性与同比增长分析、ACF与PACF定阶及SARIMA未来销量预测,并延…

2026/10/11 22:25:13 阅读更多 →
YOLOv5垃圾分类毕设实战:从环境配置到答辩提分全解析

YOLOv5垃圾分类毕设实战:从环境配置到答辩提分全解析

简介:这是一套面向高校学生与深度学习初学者的智能生活垃圾分类实战项目,基于YOLOv5目标检测框架实现,可直接用于毕业设计、期末大作业或课程设计。项目代码注释完整,新手也能读懂,部署流程简单,下载后即可…

2026/10/11 22:25:12 阅读更多 →
YOLOv8 Web端实时目标检测工程化实践

YOLOv8 Web端实时目标检测工程化实践

简介:本资源是一个基于YOLOv8框架实现的轻量级实时目标检测Web应用完整工程,面向深度学习初学者、计算机视觉课程设计与毕业设计学生,解决将先进目标检测模型封装为可交互Web服务的技术落地问题。压缩包共36个文件,含14个核心Pyth…

2026/10/11 22:25:12 阅读更多 →
微博舆情分析毕设全链路:Python爬虫与情感分析实战

微博舆情分析毕设全链路:Python爬虫与情感分析实战

简介:这是一套面向计算机专业学生与Python初学者的微博舆情分析可视化毕业设计项目,围绕爬虫采集、情感分析与数据可视化三大模块展开,适合用作毕业设计、期末大作业或课程设计参考。资源包共146个文件,约3.85MB,包含1…

2026/10/11 22:25:12 阅读更多 →
vscode设置光标快捷键上下左右移动(详细):把 keybindings.json 改到 TaoToken 的完整配置与验证

vscode设置光标快捷键上下左右移动(详细):把 keybindings.json 改到 TaoToken 的完整配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 22:25:12 阅读更多 →
YOLOv7火焰烟雾检测实战:从数据集标注到部署避坑全攻略

YOLOv7火焰烟雾检测实战:从数据集标注到部署避坑全攻略

简介:基于YOLOv7的火焰与烟雾检测方案,面向计算机视觉开发者、消防预警领域研究者、安全监控项目人员以及正在入门目标检测的学员,适合作为模型训练、推理验证和二次开发的参考基础。资源包含训练好的模型权重,下载后可直接加载推…

2026/10/11 22:24:10 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →