news 2026/10/1 4:02:40

Java人脸识别签到系统:稳定上线与生产级实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java人脸识别签到系统:稳定上线与生产级实践指南

简介:本资源是一个基于Java实现的人脸识别签到系统开源项目,面向Java中级开发者、人工智能初学者及高校课程设计实践者,解决无接触身份核验与考勤管理场景下的技术落地问题。压缩包共225个文件,含65个核心Java源码(涵盖API调用、图像处理与业务逻辑)、78个XML配置文件(Spring/Android相关)、36个PNG图标资源、18个SO本地库(支持人脸识别SDK底层调用)以及14个JAR依赖包(如Msc.jar、okhttp-3.4.1.jar等),整体大小为15.29MB。已有291人学习下载,资源结构完整,包含Gradle构建脚本、Git版本控制文件及可直接运行的工程目录,便于快速导入IDE调试;Swface-master源码仓提供了科大讯飞与Face++双API集成方案、活体检测流程、人脸比对与签到记录持久化等关键实现,是理解AI能力在Java端工程化集成的优质参考案例。

1. 为什么用 Java 做人脸识别签到系统,反而比 Python 更稳、更易上线?

去年帮一家职业培训中心做考勤改造,他们原有打卡机常被代刷、迟到漏打、统计滞后——学生进教室前刷个脸,5 秒内完成身份核验+时间戳+照片存档+自动填入 Excel 报表,这才是真实业务要的「签到」。不是实验室里跑通一张图就喊成功,而是每天 300+ 人连续 6 个月不掉链子。我们没选 Python + OpenCV 快速原型方案,而是用 Java 从头搭起整套服务:前端用 JavaFX 做本地采集界面,后端用 Spring Boot 暴露 REST 接口,人脸特征提取用 ND4J 调用 ONNX Runtime 加载轻量级 MobileFaceNet 模型,数据库用 PostgreSQL 存结构化签到记录,再加一层 Redis 缓存最近 1 小时的活体检测阈值。Java 的强类型约束让多人协作时接口契约清晰,JVM 的稳定 GC 避免了 Python 多线程下 GIL 导致的摄像头卡顿,而打包成单个 JAR 后,运维直接双击运行、Windows/Linux/macOS 三端零配置兼容——这恰恰是教育机构 IT 管理员最需要的「扔过去就能用」。如果你正面临:需要嵌入现有 Java ERP 系统、对日志审计和事务一致性有硬性要求、部署环境受限(如无 root 权限装 Python 包)、或团队主力是 Java 工程师而非算法研究员——那么「人脸识别签到系统_java」不是技术炫技,而是降低交付风险的务实选择。


2. 从零构建 Java 人脸识别签到系统:核心模块拆解与选型依据

2.1 为什么不用 OpenCV Java 绑定?而选 ONNX Runtime + ND4J 组合

OpenCV 官方 Java binding(opencv-4.8.0.jar)确实能调用CascadeClassifier做人脸检测,但其内置的LBPHFaceRecognizer或EigenFaceRecognizer在实际场景中准确率不足 72%(我们在 200 人样本库上实测),且无法支持活体检测、关键点定位等现代需求。更重要的是,OpenCV Java 版本对 CUDA 加速支持极弱,同一张 RTX 3060 显卡,在 Python 中推理耗时 42ms,在 Java 中却飙到 180ms+,根本无法支撑实时视频流处理。

我们最终采用ONNX Runtime Java SDK + ND4J 数值计算库的组合,原因有三:

  • 模型可移植性:训练好的 PyTorch 模型(如 MobileFaceNet + ArcFace head)导出为 ONNX 格式后,无需重训,Java 端直接加载推理;
  • 硬件加速可控:ONNX Runtime 支持 CPU / CUDA / DirectML 后端,通过OrtEnvironment设置ExecutionMode.ORT_SEQUENTIAL和GraphOptimizationLevel.ORT_ENABLE_ALL,在 Windows 上启用 CUDA 后,单帧推理稳定在 35~40ms(1080p 输入,batch=1);
  • 内存安全边界清晰:ND4J 的INDArray对象生命周期由 JVM 管理,避免 JNI 层指针越界导致的 JVM crash(这是 OpenCV Java binding 最频繁的崩溃根源)。

提示:不要用org.bytedeco.opencv这类第三方 JavaCV 封装,它底层仍依赖 OpenCV JNI,且版本碎片严重(opencv-4.5.5-1.5.7.jar 与 opencv-4.8.0-1.5.9.jar 的Mat内存布局不兼容,极易引发 SIGSEGV)。

2.2 摄像头采集模块:用 JavaFX MediaPlayer 替代 AWT Robot 截图

早期方案尝试用java.awt.Robot每 100ms 截取屏幕区域模拟摄像头,结果在多显示器、高 DPI(如 200% 缩放)环境下坐标错乱,且无法获取原始 YUV 流,导致人脸对齐精度下降。改用 JavaFX 的MediaPlayer+MediaView是更健壮的选择:

// 初始化摄像头(需提前安装 Webcam Capture API 0.3.12) Webcam webcam = Webcam.getDefault(); webcam.setViewSize(new Dimension(640, 480)); webcam.open(); // 将 Webcam 帧转为 JavaFX Image(避免 AWT-Swing-FX 跨线程渲染冲突) Platform.runLater(() -> { BufferedImage image = webcam.getImage(); if (image != null) { WritableImage fxImage = SwingFXUtils.toFXImage(image, null); mediaView.setImage(fxImage); // 绑定到 UI 控件 // 关键:此处拿到的 BufferedImage 是 RGB 格式,可直接送入 ONNX 模型预处理 processFrame(image); } });

该方案优势在于:

  • Webcam Capture API自动适配 UVC 协议摄像头(包括罗技 C920、海康 DS-2DE2A404IW-DE 等主流型号),无需手动解析 USB descriptor;
  • SwingFXUtils.toFXImage()保证图像数据在 JavaFX 渲染线程内完成转换,规避Graphics2D并发写冲突;
  • BufferedImage实例可直接用image.getRGB()提取像素数组,省去 OpenCVMat与BufferedImage互转的序列化开销(实测节省 12~15ms/帧)。

2.3 特征比对与阈值策略:不用固定阈值,而用动态自适应阈值

很多人误以为“人脸识别 = 提取特征向量 → 计算余弦相似度 → 判定是否大于 0.6”,但实际部署中,0.6 这个数在不同光照、角度、口罩遮挡下波动极大。我们采用分层动态阈值策略:

场景类型基准阈值动态调整逻辑触发条件
正面无遮挡0.72+0.03连续 3 帧关键点置信度 > 0.95
侧脸(yaw > 30°)0.65-0.02/每增加 5° yaw由 MediaPipe FaceMesh Java 版估算
弱光(灰度均值 < 45)0.68-0.01 × (45 - meanGray)实时计算 ROI 区域灰度直方图
戴口罩0.58锁定,禁止上调用轻量 CNN 分类器(ONNX)二分类输出

该策略封装为AdaptiveThresholdEngine类,每次签到请求都会根据当前帧质量参数实时计算本次比对阈值,而非全局硬编码。实测将误拒率(FRR)从 11.3% 降至 3.7%,同时保持误认率(FAR)< 0.02%(10 万次测试)。


3. 数据管道设计:人脸注册、特征入库与增量更新的 Java 实现

3.1 注册流程:三步完成「人-照-特征」绑定

注册不是简单拍张照存数据库,而是包含质量校验、活体验证、特征归一化三个强制环节:

  1. 质量初筛:用 OpenCV Java 计算当前帧的contrast(对比度)、sharpness(拉普拉斯方差)、illumination(ROI 平均亮度),任一指标低于阈值则提示“请靠近光源”;
  2. 活体检测:调用预训练的liveness.onnx模型(输入 112×112 归一化人脸图,输出 2 分类概率),仅当活体置信度 > 0.85 才进入下一步;
  3. 特征生成与存储:将活体图送入主模型提取 512 维特征向量,经 L2 归一化后,以 Base64 编码存入 PostgreSQL 的face_features表,并关联student_id,register_time,device_id。
CREATE TABLE face_features ( id BIGSERIAL PRIMARY KEY, student_id VARCHAR(20) NOT NULL, feature BYTEA NOT NULL, -- 存储 Base64 解码后的 float[] 二进制 register_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(), device_id VARCHAR(50), quality_score NUMERIC(3,2), -- 0.0~1.0,综合 contrast/sharpness/illumination CONSTRAINT uk_student_device UNIQUE (student_id, device_id) );

注意:feature字段必须用BYTEA类型(PostgreSQL 的二进制大对象),而非TEXT。若存 Base64 字符串,单条记录膨胀至 1.2KB(512×4 bytes → Base64 编码后约 682 chars),而BYTEA只占 2048 bytes,且 JDBCPreparedStatement.setBytes()可直接绑定float[]的ByteBuffer.array(),避免字符串编解码开销。

3.2 特征检索优化:用 KD-Tree 实现毫秒级最近邻搜索

PostgreSQL 自带cube扩展支持 kNN,但其距离计算基于欧氏距离,且索引构建慢(10 万人脸库建索引需 23 分钟)。我们改用纯内存方案:启动时加载全部特征向量构建KDTree,查询时执行knnSearch(queryVector, k=3),实测 5 万条特征下平均响应 8.2ms(i5-1135G7)。

核心代码使用smile-projection库(v2.6.0):

// 初始化 KDTree(只在 Spring Boot 启动时执行一次) private KDTree<float[]> kdtree; private Map<String, float[]> idToFeatureMap; // student_id → normalized feature @PostConstruct public void initFeatureIndex() { List<float[]> features = faceFeatureRepository.findAllFeatures(); // 从 DB 读取所有特征 String[] ids = faceFeatureRepository.findAllIds(); kdtree = new KDTree<>(features.toArray(new float[0][]), Distance.EUCLIDEAN); // 使用欧氏距离(与余弦距离等价,因已 L2 归一化) idToFeatureMap = IntStream.range(0, ids.length) .boxed() .collect(Collectors.toMap(i -> ids[i], i -> features.get(i))); } // 签到时调用 public MatchResult findTopMatch(float[] queryFeature) { // KDTree 返回 (index, distance) 数组,distance 越小越相似 Neighbor[] neighbors = kdtree.knn(queryFeature, 1); if (neighbors.length == 0) return new MatchResult(null, 0.0); int topIndex = neighbors[0].index; float distance = neighbors[0].distance; // 转换为余弦相似度:cosθ = 1 - distance²/2 (因向量已归一化) double similarity = 1.0 - distance * distance / 2.0; String matchedId = getStudentIdByIndex(topIndex); // 通过 index 查回 student_id return new MatchResult(matchedId, similarity); }

该方案舍弃了数据库层面的 ACID 保证,但换来确定性低延迟——签到是读多写少场景,特征库每日凌晨全量同步一次即可,完全可接受。

3.3 增量更新机制:避免全量重建 KDTree 的热更新方案

每次新增注册人脸,若重建整个 KDTree,5 万人库需 1.8 秒,期间签到请求将排队阻塞。我们采用双缓冲 + 差量合并策略:

  • 主KDTree(primaryTree)持续服务线上请求;
  • 后台线程维护一个deltaList(ConcurrentLinkedQueue),存放新增/更新的特征向量;
  • 每 30 分钟或deltaList.size() >= 500时,触发异步合并:
    // 合并逻辑(在独立线程池中执行) List<float[]> allFeatures = new ArrayList<>(primaryFeatures); allFeatures.addAll(deltaList); KDTree<float[]> newTree = new KDTree<>(allFeatures.toArray(new float[0][]), Distance.EUCLIDEAN); // 原子替换 primaryTree = newTree; primaryFeatures = allFeatures; deltaList.clear();

此机制确保primaryTree永不中断服务,而用户感知不到更新延迟(新注册人员 30 分钟内可被识别,业务可接受)。


4. 避坑指南:Java 人脸识别签到系统上线前必须踩过的 5 个深坑

4.1 现象:Windows 上摄像头预览黑屏,但日志无报错

原因:Webcam Capture API默认使用DirectShow作为 Windows 后端,而部分新驱动(如 Intel RealSense)仅支持Media Foundation。DirectShow尝试枚举设备失败后静默降级,导致webcam.isOpen()返回true,但webcam.getImage()始终返回null。
解决:强制指定后端,在main()方法开头添加:

System.setProperty("webcam.debug", "false"); System.setProperty("webcam.capture.api", "mediafoundation"); // 关键!

4.2 现象:活体检测模型在 Linux 服务器上加载失败,报UnsatisfiedLinkError: libonnxruntime.so

原因:ONNX Runtime Java SDK 的onnxruntime-1.16.3.jar内置libonnxruntime.so是针对 glibc 2.28+ 编译的,而 CentOS 7 默认 glibc 2.17。JVM 加载时找不到符号clock_gettime@GLIBC_2.17。
解决:下载适配旧版 glibc 的 runtime(非官方,需自行编译)或改用onnxruntime4j(纯 Java 实现,性能损失约 40%,但兼容性 100%)。我们选择后者,并在pom.xml中排除原生依赖:

<dependency> <groupId>com.microsoft.onnxruntime</groupId> <artifactId>onnxruntime</artifactId> <version>1.16.3</version> <exclusions> <exclusion> <groupId>com.microsoft.onnxruntime</groupId> <artifactId>onnxruntime_gpu</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>ai.onnxruntime</groupId> <artifactId>onnxruntime4j</artifactId> <version>0.1.0</version> </dependency>

4.3 现象:多线程并发签到时,SimpleDateFormat抛java.lang.NumberFormatException: multiple points

原因:SimpleDateFormat非线程安全,多个请求共用同一实例解析时间戳(如new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")),内部calendar字段被并发修改导致状态错乱。
解决:永远用DateTimeFormatter(Java 8+)替代:

// ✅ 正确:不可变、线程安全 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String timeStr = LocalDateTime.now().format(FORMATTER); // ❌ 错误:绝对禁止 // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 共享实例

4.4 现象:PostgreSQL 插入特征向量时偶尔卡死,pg_stat_activity显示idle in transaction

原因:JDBC 默认开启自动提交(autocommit=true),但PreparedStatement.setBytes()传入大byte[]时,PostgreSQL 驱动会启用流式传输(streaming),若网络抖动或客户端 GC 暂停,事务长时间挂起。
解决:显式关闭自动提交,并控制事务粒度:

Connection conn = dataSource.getConnection(); conn.setAutoCommit(false); // 关键! try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO face_features (student_id, feature, quality_score) VALUES (?, ?, ?)")) { ps.setString(1, studentId); ps.setBytes(2, featureBytes); // featureBytes 是 float[] 的 ByteBuffer.array() ps.setDouble(3, qualityScore); ps.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.close(); }

4.5 现象:JavaFX 界面在高 DPI 显示器上文字模糊、按钮错位

原因:Java 11+ 默认启用 HiDPI 支持,但 JavaFX 的Scene未正确设置PixelScaleFactor,导致 CSS 像素与物理像素不匹配。
解决:在Application.launch()前强制设置系统属性:

public class SignApp extends Application { @Override public void start(Stage stage) { ... } public static void main(String[] args) { // 必须在 launch() 前设置! System.setProperty("prism.allowhidpi", "true"); System.setProperty("prism.text", "t2k"); launch(args); } }

并在 CSS 中用em或%替代px单位(如font-size: 1.2em;),确保缩放自适应。


5. 生产级加固:日志审计、离线模式与防代刷的三重防线

5.1 签到事件全链路审计日志:从摄像头帧到数据库落盘的 7 个关键节点

真实业务中,教务处常质疑“某学生声称已签到但系统无记录”。我们设计了七段式审计日志,每条签到生成唯一trace_id,贯穿全部组件:

节点日志字段示例值作用
1. 帧捕获frame_ts,camera_id,resolution1712345678901,USB-CAM-01,640x480定位硬件异常
2. 质量评估contrast,sharpness,illumination0.42,128.7,63.2判定是否因环境问题拒识
3. 活体结果liveness_score,liveness_label0.92,real证明非照片攻击
4. 特征提取feature_hash,model_versiona1b2c3d4...,mobilefacenet-v2.1防特征篡改
5. 比对结果top_match_id,similarity,threshold_usedS2023001,0.732,0.715解释判定依据
6. 业务决策decision,reasonACCEPT,similarity > threshold业务层结论
7. 数据落库db_commit_ts,row_count1712345678923,1验证持久化成功

所有日志统一写入sign_audit.log,按天滚动,且每条含trace_id。当出现争议时,运维只需 greptrace_id,即可还原完整链条,无需翻查多个日志文件。

5.2 离线签到模式:断网时仍可工作,网络恢复后自动同步

教育机构常遇网络故障(如光纤被挖断),但签到不能停。我们实现本地 SQLite 缓存 + WAL 模式同步:

  • 正常联网时,签到事件直写 PostgreSQL;
  • 检测到ping postgres-host -c 1超时(>3s),自动切换至离线模式:
    • 所有签到写入本地offline_signs.db(SQLite,WAL journal mode);
    • 启动后台线程,每 30 秒尝试 reconnect,成功后执行INSERT INTO pg_table SELECT * FROM offline_table;
    • 同步完成后清空本地表,并记录sync_start_ts/sync_end_ts到审计日志。

关键保障:

  • SQLite 启用PRAGMA journal_mode=WAL,允许多线程并发读写;
  • 同步 SQL 使用INSERT ... ON CONFLICT DO NOTHING防止重复插入;
  • 本地表增加sync_status字段(pending/synced/failed),失败时保留供人工干预。

实测断网 4 小时后恢复,237 条离线记录在 8.3 秒内全部同步,无一条丢失。

5.3 防代刷终极手段:行为时序指纹 + 设备绑定

单纯比对人脸特征,无法防御“借同学手机远程刷脸”。我们叠加两层防御:

第一层:行为时序指纹
采集每次签到的 5 个时序特征:

  • frame_interval_ms:连续两帧采集间隔(正常人眨眼/微动导致 120~250ms 波动);
  • head_movement_std:10 帧内头部关键点移动标准差(代刷者通常静止不动,std < 0.8);
  • blink_frequency:60 秒内眨眼次数(真人平均 15~20 次,照片/视频为 0);
  • interaction_duration:从画面出现人脸到点击“确认”按钮的耗时(正常 1.2~3.5s,代刷常 < 0.8s);
  • touch_pattern:触摸屏设备的按压面积变化曲线(JavaFXTouchEvent可捕获)。

任一特征偏离历史分布(Z-score > 3),即标记为suspicious,触发人工复核。

第二层:设备硬件指纹绑定
不依赖 IP(校园网 NAT 共享),而采集:

  • Webcam.getDeviceName()+getVendorId()(USB 设备描述符);
  • GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()的分辨率与 DPI 组合;
  • ManagementFactory.getRuntimeMXBean().getName()(JVM PID@host,可反推主机名)。

三者哈希后存入device_fingerprint表,新设备首次签到需管理员扫码授权。某次测试中,学生用 iPad 模拟器刷脸,因getVendorId()返回0x0000(虚拟设备),系统直接拦截并短信通知管理员。


6. 性能压测与上线 checklist:从 10 人到 1000 人的平滑扩容路径

6.1 压测结果:单节点极限承载能力与瓶颈定位

我们用 JMeter 模拟 1000 并发用户(每 5 秒发起一次签到请求),在 4 核 8GB 的阿里云 ECS(CentOS 7.9)上实测:

指标50 并发200 并发500 并发1000 并发瓶颈分析
平均响应时间128ms215ms487ms超时率 12%CPU 使用率 98%,ONNX 推理线程争抢
TPS(成功)382415398352PostgreSQL 连接池满(默认 max=10)
内存占用1.2GB1.8GB2.4GB3.1GBND4JINDArray缓存未及时 GC
错误日志003 条OutOfMemoryError47 条Connection refused连接池与 JVM 堆配置不合理

根治方案:

  • ONNX 推理层:将OrtSession设为单例,复用OrtSession.SessionOptions,并设置setInterOpNumThreads(2)和setIntraOpNumThreads(2),避免线程爆炸;
  • 数据库层:HikariCP 连接池maximumPoolSize=50,connection-timeout=30000,leak-detection-threshold=60000;
  • JVM 层:启动参数-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200,并添加-Dnd4j.memory.mode=direct强制 ND4J 使用堆外内存。

调整后,1000 并发下 TPS 稳定在 480+,平均响应 320ms,超时率 < 0.3%。

6.2 上线 checklist:12 项必须验证的生产就绪项

序号检查项验证方法通过标准
1摄像头兼容性连接罗技 C920、海康 DS-2DE2A404IW、华为 SDC-4100 三款设备100% 能预览、无绿屏/花屏
2高 DPI 适配在 200% 缩放的 Surface Pro 上运行文字清晰、按钮尺寸正常、无裁剪
3断网恢复拔网线 5 分钟 → 插回 → 检查离线记录同步所有离线记录 10 秒内同步完成,无重复
4日志完整性greptrace_id查一条签到日志7 个节点日志全部存在,时间递增
5活体防攻击用打印照片、手机视频、3D 面具各测试 10 次活体检测全部拒绝,无漏判
6特征库容量向库中注入 10 万条特征KDTree 构建 < 90 秒,查询 P99 < 15ms
7JVM 内存泄漏运行 72 小时,每小时 dump heapINDArray对象数稳定,无持续增长
8数据库连接泄漏持续签到 1 小时,观察pg_stat_activityidle状态连接数 ≤ 5(pool size × 0.1)
9时间同步修改系统时间 ±10 分钟签到时间戳仍准确(依赖 NTP 客户端)
10多显示器适配在双屏(主屏 1920×1080,副屏 1366×768)下启动界面始终显示在主屏,不跨屏错位
11中文路径支持将 JAR 放在D:\人脸识别系统\路径下运行无FileNotFoundException,日志路径可写
12权限最小化以非 root 用户启动,禁用 sudo所有功能正常,无权限拒绝日志

最后说句实在话:这套方案我们已在 3 所职业院校落地,最长稳定运行 14 个月。最大的教训是——别迷信“准确率 99.9%”的论文指标,真实世界里,把活体检测的假阳性压到 0.1%、把弱光场景的召回率提到 92%、让运维人员双击 JAR 就能跑起来,这些才是决定项目成败的细节。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:02:18

高校Wi-Fi 7全覆盖建设实战:从技术选型到落地验收

1. 校园网的真实瓶颈&#xff1a;为什么Wi-Fi 5/6的升级被提前提上日程在高校信息中心待过的人都有这种体会&#xff1a;每年新生入学季&#xff0c;就是一次网络运维的“大考”。湖职这次启动Wi-Fi 7全覆盖建设之前&#xff0c;我们其实已经做过一轮摸底测试。测试结果不太乐观…

作者头像 李华
网站建设 2026/10/1 4:02:02

大西洋花园马德拉:徒步levada+自驾环岛深度攻略

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

作者头像 李华
网站建设 2026/10/1 4:01:19

Linux下LAMMPS安装全攻略:从环境配置到GPU加速

1. 安装之前&#xff0c;你得先想清楚这三件事这几年分子动力学模拟越来越普及&#xff0c;LAMMPS作为一款开源免费、生态庞大、社区活跃的软件&#xff0c;几乎成了做材料、化学、生物、物理模拟的人绕不开的工具。我见过太多人一上来就搜“lammps安装教程”&#xff0c;照着别…

作者头像 李华
网站建设 2026/10/1 4:01:11

跨平台开发对抗赛:SQLite数据层与Godot物理回滚的实战拆解

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

作者头像 李华