SpringBoot整合Activiti与Shiro:工作流权限审批系统实战
1. 为什么要在业务系统里同时引入工作流与权限框架做过企业级业务系统的人大概都有这种体会一个审批类需求刚上线时流程简单得用几个状态字段就能糊弄过去可一旦业务跑起来节点越加越多、审批人越来越复杂、还要支持驳回、加签、转办、撤回这时候再靠手写状态机就是给自己挖坑。我见过太多项目在第二期需求评审时被业务方一句“这个流程能不能改一下”直接问懵最后只能推倒重来。这个项目要解决的核心问题就是把Activiti工作流引擎和SpringBoot、Shiro权限框架这三样东西揉到一起搭出一套能真正落地的审批系统骨架。Activiti负责流程的定义、流转、任务分配和历史追溯SpringBoot负责把整个工程的组织结构、依赖注入、自动配置打理干净Shiro负责登录认证、角色授权、细粒度到按钮级别的权限控制。三者各司其职但整合起来有一堆坑尤其是任务候选人、候选组和Shiro角色之间的映射关系处理不好就会出现“流程走到某节点却没人能审批”的尴尬局面。这套东西适合谁看如果你已经写过SpringBoot项目对MVC那一套不陌生但一提到工作流就只知道画个流程图却不知道怎么和业务表关联那这篇内容就是给你准备的。如果你还在用if-else判断审批状态那更应该看看正规军是怎么打的。我会把整合过程中的设计思路、关键配置、踩过的坑都摊开讲代码片段可以直接抄参数选择我会告诉你为什么这么定。2. 整体架构设计与技术选型考量2.1 三层职责划分的底层逻辑先把三个框架的边界划清楚这是整个项目不跑偏的前提。很多整合失败的案例根源就在于职责混乱——有人把权限判断写进流程监听器里有人把流程节点状态塞进Shiro的授权信息里最后代码变成一团乱麻。我的划分原则是这样的Shiro管“谁能进这个系统、谁能点这个按钮”Activiti管“这个单据现在走到哪、下一步该谁处理”。两者通过用户身份这个纽带连接。具体来说Shiro的Subject里存着当前登录用户的身份信息Activiti在创建任务时通过AssignmentHandler或者任务监听器把这个身份信息转换成流程的候选人。SpringBoot在这里的角色是粘合剂。它通过starter机制把Activiti的ProcessEngine、Shiro的SecurityManager都纳入IOC容器管理让它们能共享数据源、共享事务管理器。这一点非常关键因为流程流转和业务数据更新必须在同一个事务里否则就会出现“任务已创建但业务单据状态没改”的数据不一致。2.2 版本选择与依赖冲突处理版本这块我踩过血泪教训。Activiti 6和Activiti 7的API差异很大Activiti 7走的是SpringBoot 2.x的路线但它的云版本Activiti Cloud又是另一套东西别搞混。我建议用Activiti 6.0.0配合SpringBoot 2.3.x这个组合最稳社区资料也最全。Shiro用1.7.x以上版本低版本有反序列化相关的安全隐患这个不用我多说。依赖冲突主要集中在两点一是Activiti自带的mybatis版本和SpringBoot管理的版本可能打架需要在pom里显式排除再引入二是Shiro的starter和SpringBoot的web starter在过滤器注册顺序上要协调。我的做法是在pom里把Activiti的mybatis依赖排除掉统一用SpringBoot管理的版本然后在配置类里手动调整FilterRegistrationBean的顺序确保Shiro的过滤器在Activiti的Web过滤器之前执行。dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter-basic/artifactId version6.0.0/version exclusions exclusion groupIdorg.mybatis/groupId artifactIdmybatis/artifactId /exclusion /exclusions /dependency注意排除mybatis后要确保SpringBoot的mybatis starter版本在3.5.x以上否则Activiti的查询会报奇怪的SQL语法错误。2.3 数据库表结构规划思路Activiti默认会建25张表ACT_开头这些表不要动也不要去改它的字段。业务表单独建通过BUSINESS_KEY字段和流程实例关联。我见过有人为了“省事”把业务数据直接塞进Activiti的变量表里结果流程一多变量表膨胀到几千万行查询慢得没法看。正确的做法是业务主表存核心字段流程实例ID和任务ID作为外键关联字段存在业务表里。这样查“我的待办”时先用Activiti的API查出任务列表拿到businessKey再去业务表里查详情。虽然多一次查询但数据边界清晰后期维护成本低得多。3. Shiro权限体系与Activiti任务分配的整合方案3.1 认证流程的定制化改造Shiro默认的认证是UsernamePasswordToken但企业系统往往需要支持多种登录方式比如手机号验证码、工号密码。我的做法是自定义一个Realm继承AuthorizingRealm在doGetAuthenticationInfo方法里根据登录类型走不同的校验逻辑。这里有个细节要注意Activiti在创建任务时如果指定了候选人它会去查用户表。但Activiti自带的IdentityService管理的用户表和Shiro的Realm是两套东西。我的建议是不要用Activiti的IdentityService而是通过自定义的TaskListener在任务创建时从Shiro的Realm里查用户信息然后手动设置候选人。这样用户数据只有一份避免同步问题。public class CustomRealm extends AuthorizingRealm { Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } }3.2 角色与流程候选组的映射策略这是整个整合中最容易出问题的地方。Activiti的候选人可以是单个用户assignee也可以是候选组candidateGroup。Shiro的角色体系里一个用户可以有多个角色。那么问题来了流程定义时写的候选组到底对应Shiro的哪个概念我的方案是在Shiro里维护一套“业务角色”这套角色和Activiti的候选组一一对应。比如流程里写candidateGroupsdept_manager那Shiro里就有一个角色叫dept_manager。用户被赋予这个角色后就能看到对应的待办任务。查询待办的时候不能直接用taskService.createTaskQuery().taskCandidateUser(username)因为这样查不到候选组的任务。正确的姿势是先把用户的所有角色查出来然后ListString roleCodes shiroService.getRoleCodesByUser(username); ListTask tasks taskService.createTaskQuery() .taskCandidateUser(username) .taskCandidateGroupIn(roleCodes) .orderByTaskCreateTime().desc() .list();提示taskCandidateUser和taskCandidateGroupIn是OR关系不是AND所以这样写能同时查到个人任务和角色任务。3.3 任务监听器中的权限校验流程流转到某个节点时有时候需要动态判断审批人。比如金额小于1万走部门经理大于1万走总监。这种逻辑不要写在流程定义里而是通过任务监听器在运行时动态设置。我通常实现一个TaskListener在notify方法里根据流程变量判断然后调用delegateTask.addCandidateGroup()或者delegateTask.setAssignee()。这里要注意监听器里不要做太重的操作比如查数据库、调远程接口因为监听器是在流程引擎的事务里执行的耗时太长会拖垮整个流程。public class DynamicAssigneeListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { Double amount (Double) delegateTask.getVariable(amount); if (amount ! null amount 10000) { delegateTask.addCandidateGroup(director); } else { delegateTask.addCandidateGroup(dept_manager); } } }4. 完整实操流程与关键环节实现4.1 工程搭建与基础配置新建SpringBoot工程pom里引入web、mybatis、mysql、shiro-spring-boot-web-starter、activiti-spring-boot-starter-basic。application.yml里配置数据源和Activiti的自动建表策略spring: datasource: url: jdbc:mysql://localhost:3306/workflow_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 activiti: database-schema-update: true check-process-definitions: true process-definition-location-prefix: classpath:/processes/ history-level: fullhistory-level设为full是为了能查历史审批记录如果只设为audit有些变量查不到。database-schema-update设为true会自动建表生产环境建议设为false用SQL脚本手动初始化。4.2 流程定义文件的编写要点在resources/processes目录下放bpmn文件。我习惯用Eclipse或者IDEA的Activiti插件画流程图但生成的XML要检查几个地方userTask的activiti:candidateGroups属性、sequenceFlow的条件表达式、以及startEvent的formKey。一个典型的请假流程bpmn片段userTask iddeptApprove name部门经理审批 activiti:candidateGroupsdept_manager extensionElements activiti:taskListener eventcreate classcom.demo.listener.DynamicAssigneeListener/ /extensionElements /userTask注意bpmn文件里的中文名称要确保文件编码是UTF-8否则部署时会报XML解析错误。4.3 业务表与流程的关联实现业务表设计一个leave_apply表字段包括id、user_id、days、reason、status、process_instance_id、create_time。status字段用来做列表展示实际的状态以Activiti的流程实例为准。发起流程的代码Transactional public void startLeaveProcess(LeaveApply apply, String username) { // 1. 保存业务数据 apply.setStatus(审批中); leaveApplyMapper.insert(apply); // 2. 设置流程变量 MapString, Object variables new HashMap(); variables.put(applyUser, username); variables.put(days, apply.getDays()); variables.put(amount, apply.getAmount()); // 3. 启动流程实例businessKey用业务表主键 String businessKey String.valueOf(apply.getId()); ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveProcess, businessKey, variables); // 4. 回写流程实例ID apply.setProcessInstanceId(instance.getId()); leaveApplyMapper.updateById(apply); }这里Transactional是必须的保证业务数据和流程实例同时成功或同时失败。businessKey用业务表主键这样从任务反查业务数据时直接businessKey就能拿到。4.4 待办任务与已办任务的查询实现待办查询前面已经讲了核心是候选人候选组的OR查询。已办任务查询用historyService.createHistoricTaskInstanceQuery().taskAssignee(username).finished()。这里有个性能优化点如果历史任务很多不要一次性查出来要分页。Activiti的查询API支持listPage(offset, limit)。我一般会封装一个TaskQueryService把待办、已办、流程跟踪都封装成方法。流程跟踪用historyService.createHistoricActivityInstanceQuery().processInstanceId(pid).orderByHistoricActivityInstanceStartTime().asc()这样能拿到完整的流转路径前端画个时间轴就能展示。4.5 审批操作的完整实现审批通过Transactional public void approve(String taskId, String comment, String username) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if (task null) { throw new RuntimeException(任务不存在或已处理); } // 添加审批意见 taskService.addComment(taskId, task.getProcessInstanceId(), comment); // 完成任务 taskService.complete(taskId); // 更新业务状态 updateBusinessStatus(task.getProcessInstanceId()); }驳回稍微复杂一点需要先找到目标节点然后用runtimeService.createChangeActivityStateBuilder().moveActivityIdTo(currentActId, targetActId).changeState()。这个API是Activiti 6才有的5.x版本要用其他方式实现。5. 常见问题排查与避坑经验实录5.1 任务查不到候选人问题这是最高频的问题。现象是流程启动了但待办列表是空的。排查步骤先查ACT_RU_TASK表看ASSIGNEE_和CANDIDATE_字段有没有值。如果ASSIGNEE_为空且候选组也没设置那就是流程定义里没写候选人。如果候选组设置了但查不到检查Shiro里用户的角色编码和流程里的候选组编码是否完全一致大小写敏感。还有一个隐蔽的坑taskCandidateGroupIn传的是角色编码列表但如果用户有角色但角色编码是中文Activiti查询时会因为编码问题查不到。所以角色编码一律用英文。5.2 流程变量类型不匹配问题Activiti的变量是强类型的。如果你在启动时put了一个Integer在监听器里用getVariable(amount)强转成Double会抛ClassCastException。我的经验是金额类变量统一用Double数量类统一用Integer字符串统一用String在流程定义的条件表达式里也要对应类型。条件表达式写错也是常见问题。比如${amount 10000}如果amount是String类型比较结果可能不符合预期。所以变量类型一定要在启动流程前就确定好。5.3 事务与异步执行的冲突Activiti的异步任务比如定时边界事件默认是在独立事务里执行的这时候Shiro的SecurityManager可能拿不到当前用户上下文。如果异步任务里需要用户信息要在流程变量里把用户信息传进去而不是从Shiro的ThreadLocal里取。另外Transactional和Activiti的complete方法一起用时要注意事务传播级别。如果业务方法本身有事务complete会加入当前事务这是对的。但如果业务方法没有事务complete会自己开一个事务这时候如果后续业务操作失败流程已经流转了数据就不一致了。5.4 常见问题速查表问题现象可能原因排查方法解决方案待办列表为空候选人/候选组未设置查ACT_RU_TASK表检查bpmn定义和监听器流程启动报错流程定义未部署查ACT_RE_PROCDEF表检查processes目录和部署配置审批后状态未更新事务未生效检查Transactional注解确保业务方法和complete在同一事务历史记录查不到history-level配置过低查ACT_HI_ACTINST表改为full级别候选人查询慢候选组过多看SQL执行计划加索引或改用assignee5.5 独家避坑技巧第一个技巧流程定义文件不要频繁改ID。流程定义的key一旦上线就不要改改了会导致历史流程实例关联不上。如果非要改用版本控制Activiti会自动生成新版本。第二个技巧审批意见用addComment而不是setVariable。addComment会存到ACT_HI_COMMENT表和任务关联查询方便。setVariable存到变量表数据量大了查询慢。第三个技巧前端传taskId时要做权限校验。不能只靠前端隐藏按钮后端在complete之前要校验当前用户是否有权限处理这个任务。校验逻辑是查任务的候选人和候选组判断当前用户是否在其中。private void checkTaskPermission(String taskId, String username) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if (task null) { throw new RuntimeException(任务不存在); } if (username.equals(task.getAssignee())) { return; } ListString roleCodes shiroService.getRoleCodesByUser(username); ListIdentityLink links taskService.getIdentityLinksForTask(taskId); boolean hasPermission links.stream() .anyMatch(link - candidate.equals(link.getType()) (username.equals(link.getUserId()) || roleCodes.contains(link.getGroupId()))); if (!hasPermission) { throw new RuntimeException(无权处理该任务); } }这个校验看起来简单但能挡住大部分越权操作。我见过有系统因为没做这个校验普通用户通过改URL就能审批总监节点的任务这是很严重的安全漏洞。6. 权限缓存与流程性能优化6.1 Shiro缓存配置Shiro默认每次授权都会查数据库用户一多数据库压力很大。必须配缓存。我用的是EhCache在ShiroConfig里设置securityManager.setCacheManager(new EhCacheManager())。缓存的是AuthorizationInfo也就是用户的角色和权限列表。用户角色变更时要手动清缓存调用shiroService.clearAuthorizationCache(username)。Activiti这边也有缓存主要是流程定义的缓存。默认是开启的不用额外配置。但如果流程定义文件很大部署时会慢这是正常的。6.2 待办查询的性能优化待办查询是最高频的操作优化空间很大。第一给ACT_RU_TASK表的ASSIGNEE_和CANDIDATE_字段加索引。第二如果候选组很多考虑用taskCandidateGroupIn的时候限制数量或者改用自定义SQL直接查。第三待办列表不要查全量字段只查需要的。我实测过一个场景5000个用户每人平均20个待办用Activiti原生API查询平均耗时200ms加了索引后降到50ms。如果还嫌慢可以把待办数据同步到Redis里用空间换时间。6.3 历史数据归档策略ACT_HI_开头的表会随着流程运行不断增长。我的做法是每季度归档一次把已完成超过半年的流程实例数据迁移到历史归档表然后从Activiti的历史表里删除。删除的时候要注意外键关联先删ACT_HI_DETAIL再删ACT_HI_COMMENT最后删ACT_HI_PROCINST。归档脚本用存储过程或者定时任务实现不要手动删容易删出问题。归档前一定要备份这是铁律。7. 项目扩展方向与个人实操体会这套骨架搭好之后扩展方向其实很多。比如加个流程设计器让业务人员自己画流程图前端用bpmn-js后端提供部署接口。再比如加个消息通知任务创建时发邮件或者站内信用TaskListener的create事件触发。还可以加流程监控用Activiti的historyService查所有运行中的流程实例统计每个节点的平均耗时找出瓶颈节点。我在实际项目里踩过最大的坑是流程定义和业务代码的耦合。一开始我把业务逻辑写在Service里流程流转也写在Service里后来流程一改Service就要跟着改。后来我把流程相关的操作全部抽到ProcessService里业务Service只负责业务数据两者通过businessKey关联改流程的时候业务代码基本不用动。还有一个体会是不要试图用工作流解决所有问题。有些简单的状态流转用状态机就够了上工作流反而增加复杂度。工作流适合节点多、审批人动态、需要历史追溯的场景。选型的时候要克制不要为了用而用。最后分享一个小技巧Activiti的runtimeService.getVariables()返回的是Map如果流程变量很多每次查都全量返回性能不好。可以用getVariablesLocal()只查当前节点的局部变量或者用getVariable()单个查。这个细节在变量多的流程里能省不少时间。

相关新闻

计算机二级C语言程序修改题:高频错误类型与考场排查技巧

计算机二级C语言程序修改题:高频错误类型与考场排查技巧

1. 从一道程序修改题说起:为什么这类题总让人栽跟头计算机二级C语言考试里,程序修改题是很多人的心头痛。选择题可以蒙,填空题可以猜,但程序修改题摆在那里,代码一行行看过去,明明感觉没什么问题&#xff0…

2026/10/13 14:01:40 阅读更多 →
手写朴素贝叶斯实现豆瓣影评情感分类:从数据清洗到模型评估

手写朴素贝叶斯实现豆瓣影评情感分类:从数据清洗到模型评估

简介:面向高校程序设计课程设计场景,这份压缩包围绕豆瓣影评情感分类任务,提供基于朴素贝叶斯算法的完整Python实现方案,覆盖从原理学习到编码实践的完整链路。资源共10个文件,包含4个Python脚本、1个Jupyter notebook…

2026/10/13 14:01:44 阅读更多 →
用Claude精准分析老项目技术栈:从目录到依赖的实战方法论

用Claude精准分析老项目技术栈:从目录到依赖的实战方法论

接手老项目,最让人心里没底的往往不是业务逻辑有多绕,而是你根本不知道这个项目是用什么技术栈堆起来的。尤其是那种没有README、没有文档、连提交记录都断断续续的仓库,打开的一瞬间,目录结构铺在眼前,就像一盘没标食…

2026/10/13 14:00:34 阅读更多 →

最新新闻

Service层命名规范:从见名知意到代码可维护性

Service层命名规范:从见名知意到代码可维护性

接手一个业务系统,我最怕看到的不是复杂算法,而是那种一打开就让人头晕的 Service 实现类。2000 行起步,方法名字从 process 到 handle 再到 doSomething,翻遍整个类也猜不出某个方法是干嘛的。命名这种事,看起来不影响…

2026/10/13 17:55:28 阅读更多 →
Windows内核模块枚举与隐藏原理深度解析

Windows内核模块枚举与隐藏原理深度解析

1. 这不是“找模块”,而是在内核内存里做一次精准外科手术“枚举和隐藏内核模块”——这八个字在安全研究、驱动开发或系统调试场景中,绝不是一句轻飘飘的技术术语。它背后是一整套对Windows内核运行时状态的深度观测与主动干预能力。我第一次在某高校实…

2026/10/13 17:55:28 阅读更多 →
Hibernate二级缓存Provider详解:从原理到选型与排坑

Hibernate二级缓存Provider详解:从原理到选型与排坑

被人突然问起“Hibernate的caching provider是什么”,我第一反应是:能问出这个词的人,多半已经看到配置文件里那一堆以hibernate.cache开头的键,但又没搞懂这些键到底在切换什么。这篇文章就把这个词拆开讲清楚,重点解…

2026/10/13 17:55:28 阅读更多 →
电能质量复合扰动识别:混沌集成决策树实战解析

电能质量复合扰动识别:混沌集成决策树实战解析

简介:这是一份面向电能质量复合扰动识别研究的MATLAB完整源程序包,对应知网论文《基于混沌集成决策树的电能质量复合扰动识别》,可作为电气工程、信号处理及机器学习方向的毕业设计或课题研究参考,帮助复现算法并开展对比实验。程…

2026/10/13 17:55:28 阅读更多 →
CentOS局域网DNS搭建:BIND9多域名权威解析实战

CentOS局域网DNS搭建:BIND9多域名权威解析实战

1. 为什么在CentOS上自建局域网DNS不是“多此一举”,而是刚需落地的第一步很多人看到“CentOS搭建局域网DNS”第一反应是:不就是改个/etc/resolv.conf?内网查个主机名用/etc/hosts不就完事了?我试过——在某高校实验室的物联网教学…

2026/10/13 17:55:28 阅读更多 →
ActiveMQ consumerCount精准获取与实战调优指南

ActiveMQ consumerCount精准获取与实战调优指南

1. 项目概述:为什么“获取消费者数量”是 ActiveMQ 运维与调优的黄金指标在某公司消息中间件团队的实际运维中,我接手的第一个紧急故障就和consumerCount直接相关——一个订单履约服务突然延迟飙升,下游库存扣减平均耗时从200ms暴涨到8秒。排…

2026/10/13 17:54:25 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →