news 2026/9/25 6:29:27

SSM+MySQL在线收银系统源码拆解:事务、状态机与报表实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+MySQL在线收银系统源码拆解:事务、状态机与报表实战

简介:一份基于SSM+MySQL的在线收银系统毕业设计源码包,面向Java Web学习者、高校学生和小型零售开发人员,适用于超市、便利店等门店日常收银和进销存管理。覆盖系统用户管理、员工管理、用户管理、商品类别与商品管理、入库管理、销售管理和销售统计等模块,权限分配、商品建档、库存入库、销售开单、数据统计均有完整实现,可同时服务毕业设计、课程设计和实际轻量部署。压缩包约23.97MB,包含Java后端源码、SSM框架配置、MySQL数据库脚本、说明文档和LW论文材料,代码结构规范、注释清晰,便于阅读和二次开发。已有78人学习。通过研读源码,可掌握SSM分层架构、MyBatis持久化、订单与统计业务流的编写方式,并结合说明文档完成从环境搭建到系统部署的完整流程,适合需要实战参考的开发者和学生。

1. 这套在线收银系统源码,值得你花两天时间拆一遍

如果你正在做 Java 方向的毕业设计或课程设计,大概率会被一个问题卡住:选什么项目才能同时满足“代码量够、技术栈常见、答辩时讲得清楚”这三个条件。很多同学一上来就奔着秒杀系统、电商中台这种大厂业务去,结果写到一半发现事务、并发、权限全在硬凑,最后只能靠复制粘贴撑过答辩。相比之下,SSM + MySQL 的在线收银系统反而是被低估的好选择——业务链路完整,从前台选品到后台订单统计一条线走完,该有的注解、映射、状态流转一个不缺,又能控制在两周内复现完毕。

这套收银系统源码的核心价值在于它把「SSM 到底怎么串起来」讲明白了。很多教程只教你单独用 Spring、SpringMVC、MyBatis,但真正落地时,三个框架的整合细节才是坑最密集的地方。而收银系统的场景又天然覆盖了商品管理、购物车、订单、支付状态、库存扣减这些高频模块,你把这些代码吃透,等于把 Java 面试里常问的 Spring 容器、MyBatis 动态 SQL、事务传播机制全部过了一遍。适合谁?适合正在准备毕设/课设的在校生,也适合刚工作想快速补一套完整 SSM 工程结构的初级开发。

2. SSM 整合为什么是收银系统的最优解:选型理由与数据库设计

2.1 为什么是 SSM:Spring 管对象、SpringMVC 管请求、MyBatis 管 SQL

收银系统的核心诉求是「把一次按键操作变成一串稳定的数据库变更」。用户在收银台点一下「结算」,背后涉及商品价格读取、库存校验、订单记录、明细拆分、支付状态更新,这一串操作里任何一步失败,都不能让系统出现「订单生成了一半」这种脏数据。Spring 的声明式事务正好能把这串操作包在一个事务里,任何一个环节报错就整体回滚,这是你在答辩时最能讲出深度的点。

SpringMVC 管的是前端页面和后台接口之间的映射。收银台页面提交一个结算请求,DispatcherServlet 根据 URL 找到对应的 Controller 方法,把请求参数自动封装成对象,再返回逻辑视图名。MyBatis 则把 SQL 从业务逻辑里拆出来,写在 Mapper XML 里——这比 Hibernate 的全自动 ORM 更可控,因为复杂报表查询(比如按时间段统计营业额)你可以直接写原生 SQL 优化,而不是绕圈子拼 HQL。这套组合的另一个优点是「面试八股文里全是它」:IoC、AOP、动态代理、#{} 与 ${} 的区别,这些你答辩前准备的问题,代码里全都有对应实现。

2.2 数据库设计:六张表和一条完整的状态链

收银系统的表结构不像电商那样动辄几十张表,但麻雀虽小五脏俱全。原工程的 SQL 脚本里包含核心业务表,我建议你拿到源码先把表结构理清楚,不要急着跑起来。理表的过程就是你准备答辩「数据库设计」环节的过程。

表名核心字段作用
t_userid、username、password、role收银员登录与权限区分
t_categoryid、name商品分类,如饮品、主食
t_productid、name、price、stock、category_id商品基本信息与库存
t_cartid、product_id、quantity临时购物车记录
t_orderid、order_no、total_price、status、pay_time订单主表
t_order_itemid、order_id、product_id、quantity、price订单明细快照

这里的核心设计在于 t_order 和 t_order_item 是主从表关系,订单记录的是「一次交易的总价和状态」,明细记录的是「这次交易里每一件商品的单价和数量」。为什么要拆两张表?因为商品表里的价格会被修改,但订单生成后,用户以什么价格买了什么,必须永久留存——所以明细表里要冗余一份 price 快照,而不是下单时去关联查询商品表当前价格。这个设计你在答辩时主动提出来,评委老师会认为你理解「历史数据不可变」这一原则。

状态流转是另一个值得展开的细节。订单状态我用整数常量表示,避免魔法值散落在代码里。0 表示待支付,1 表示已支付、待取货,2 表示已完成(已取货),3 表示已取消(超时未支付或收银员主动取消)。四态流转限制为:只能从 0 走到 1 或 3,从 1 走到 2,不允许跳转。在 Service 层更新状态时,先查询当前状态再判断是否能转向目标状态,而不是直接执行 update——这个校验逻辑是你的「后悔药」,能挡住大量并发情况下的状态错乱。

2.3 工程结构:按包拆职责,别把所有类堆在一起

拿到源码后先看 src 目录下的包结构。我拆过的 SSM 毕设项目,最容易出现的问题就是 Controller 里塞了 SQL、Service 形同虚设。这套收银源码的包划分比较规范,建议你按这个顺序去读:

  • controller 包:只做参数接收和视图转发,不写任何业务逻辑
  • service 包 + impl 子包:业务核心,事务注解全部加在 impl 实现类上
  • dao 包(Mapper 接口):只声明方法,SQL 写在 resources 下的 mapper XML 里
  • entity 包:对应数据库表字段的实体类
  • common 包 / util 包:统一返回结果、时间工具、常量定义

读代码时先盯 service/impl 下的 OrderServiceImpl.java 和 CartServiceImpl.java,这两个是业务最厚的部分。Controller 反而可以快速扫过,因为它的逻辑基本是「接收参数 → 调用 service → 把结果塞进 ModelAndView」。工程里 github 上常见的分包方式是把 mapper 接口和 XML 分开放在 main/java 和 main/resources 两个目录,路径要保持一致,否则启动时 MyBatis 扫不到 XML 直接报Invalid bound statement (not found)。

3. 把收银流程跑通:从选品到订单生成的核心代码拆解

3.1 收银主流程:先看 Controller,再看 Service,别颠倒

用户在收银台点「结算」时,前端页面会发起一个 POST 请求到结算接口,下面这段代码就是整套系统里「含金量最高」的部分,你答辩时大概率会被点名讲解。先从 Controller 层看起:

@Controller @RequestMapping("/cashier") public class CashierController { @Autowired private OrderService orderService; // 结算入口:接收购物车里的商品数组,生成订单 @RequestMapping(value = "/checkout", method = RequestMethod.POST) public String checkout(@RequestParam("productId") Integer[] productIds, @RequestParam("quantity") Integer[] quantities, Model model) { // 把两个数组组装成购物车条目集合,传给 service 层处理 List<CartItem> items = new ArrayList<>(); for (int i = 0; i < productIds.length; i++) { CartItem item = new CartItem(); item.setProductId(productIds[i]); item.setQuantity(quantities[i]); items.add(item); } Order order = orderService.createOrder(items, getCurrentUserId()); model.addAttribute("order", order); return "cashier/order_result"; } }

这段代码的核心逻辑很简单:把页面传过来的两个等长数组(商品 ID 数组和对应数量数组)拼成一个购物车条目列表,交给 Service 层。注意这里用@RequestParam("productId") Integer[]来接批量参数,对应前端页面里 checkbox 或循环 input 的 name 属性,如果你前端 input 的 name 写成了productIds,这里就接收不到值,会报 400 参数缺失错误。这是一个非常容易翻车的细节。

真正干活的在 OrderService 的实现类里,事务注解打在方法上,保证生成订单、扣库存、清购物车三步要么全成功要么全回滚:

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Autowired private ProductMapper productMapper; @Autowired private CartMapper cartMapper; @Override @Transactional(rollbackFor = Exception.class) public Order createOrder(List<CartItem> items, Integer userId) { // 1. 计算订单总价,这里必须用 BigDecimal,用 double 会丢精度 BigDecimal totalPrice = new BigDecimal("0.00"); for (CartItem item : items) { Product product = productMapper.selectByPrimaryKey(item.getProductId()); // 2. 校验库存,防止超卖 if (product.getStock() < item.getQuantity()) { throw new RuntimeException("商品「" + product.getName() + "」库存不足"); } // 3. 单价快照:以商品表当前价格为准存入订单明细 BigDecimal subtotal = product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())); totalPrice = totalPrice.add(subtotal); } // 4. 生成订单主表记录,状态初始为 0(待支付) Order order = new Order(); order.setOrderNo(generateOrderNo()); // 时间戳 + 随机数,保证唯一 order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); // 5. 逐条插入订单明细 for (CartItem item : items) { Product product = productMapper.selectByPrimaryKey(item.getProductId()); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); // 6. 扣减库存 productMapper.reduceStock(product.getId(), item.getQuantity()); } // 7. 清空该用户的购物车 cartMapper.deleteByUserId(userId); return order; } }

这段代码里有几个地方值得你在答辩或面试时主动展开。第一,为什么总价和单价都用 BigDecimal 而不是 double?因为 double 在二进制里无法精确表示 0.1,比如 0.1 + 0.2 的结果是 0.30000000000000004,放在金额上就是致命的精度错误。Java 面试题里但凡是金融场景,问 8 大基本类型陷阱,必考这个点。第二,@Transactional(rollbackFor = Exception.class)为什么要显式指定 rollbackFor?因为 Spring 默认只在遇到 RuntimeException 时才回滚,遇到受检异常不会回滚,如果你抛出去的是一个自定义受检异常,事务照样提交,脏数据就落库了。第三,库存扣减用的 SQL 是UPDATE t_product SET stock = stock - #{count} WHERE id = #{id} AND stock >= #{count},这一步把「判断库存够不够」和「扣减」放在一条 SQL 里,是防超卖的常用做法。

3.2 模拟支付与状态推进:不用接支付 SDK,但状态机要严谨

毕业设计场景下不需要真实接入微信支付或支付宝,工程里通常是「模拟支付」——收银台点「确认收款」,直接把订单状态从待支付改成已支付。但越简单的地方越容易出问题,状态更新必须带前置条件校验,这个习惯能让你以后接真实支付 SDK 时不踩坑:

@Override @Transactional public boolean payOrder(Integer orderId, Integer userId) { // 查询当前订单 Order order = orderMapper.selectByPrimaryKey(orderId); // 校验订单归属,防止别的收银员操作别人的单子 if (order == null || !order.getUserId().equals(userId)) { throw new RuntimeException("订单不存在或无权操作"); } // 状态机校验:只有待支付(0)才能支付,已支付或已取消再点支付就是重复操作 if (order.getStatus() != 0) { throw new RuntimeException("当前订单状态不可支付"); } // 执行状态更新 Order update = new Order(); update.setId(orderId); update.setStatus(1); update.setPayTime(new Date()); return orderMapper.updateByPrimaryKeySelective(update) > 0; }

核心就一句话:更新状态前必须 select 出来看一眼现状,再决定能不能 set 新状态。很多毕设代码里直接UPDATE t_order SET status = 1 WHERE id = #{id},用户连点两次「确认收款」不会报错,但会重复触发两次支付回调——你把if (order.getStatus() != 0)这个校验写在职业生涯里第一个正式项目的接口里,能少挨不少骂。实际的代码里还有一层校验是时间:创建超过 30 分钟未支付的订单视为超时,超时订单不能支付,只能走取消接口。这个逻辑放在定时任务里扫描,还是放在支付时懒校验,两种方案都能讲出道理,工程里默认用的是懒校验,答辩时可以提一下优化方向。

3.3 商品管理与报表查询:MyBatis 动态 SQL 的练兵场

收银系统的后台功能主要是商品 CRUD 和订单统计。商品列表通常有筛选条件:按分类、按价格区间、按关键字搜索。这种「条件可选、数量不定」的查询,正好用到 MyBatis 动态 SQL。工程 mapper XML 里有一段很典型的<where>+<if>写法:

<select id="selectByCondition" resultType="com.example.entity.Product"> SELECT id, name, category_id, price, stock, status FROM t_product <where> <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="minPrice != null"> AND price &gt;= #{minPrice} </if> <if test="maxPrice != null"> AND price &lt;= #{maxPrice} </if> </where> ORDER BY id DESC </select>

<where>标签会自动把第一个多余的 AND 去掉,<if>标签则实现「这个参数传了就拼条件,没传就跳过」。注意&gt;=和&lt;=是 XML 里的转义写法,直接用>=会让 XML 解析器报错。日报表统计则用到了 SQL 里的日期函数和分组聚合,比如「统计今天每小时的营业额」,核心 SQL 大致是:

<select id="selectHourlyReport" resultType="map"> SELECT HOUR(pay_time) AS hour, SUM(total_price) AS amount FROM t_order WHERE status = 1 AND pay_time &gt;= #{startTime} AND pay_time &lt;= #{endTime} GROUP BY HOUR(pay_time) ORDER BY hour </select>

这种报表查询直接用 Mapper 返回List<Map<String, Object>>,省去为临时结果创建实体类的工作。你在答辩时可以说:「这里没有用实体类接收,因为报表结构不固定,用 Map 更灵活,后续如果要扩展成图表数据,直接加工 Map 就行。」——这句话会显得你有真实项目经验,而不是只会照着模板敲 CRUD。

4. 避坑指南:SSM + MySQL 整合过程中的五个高频翻车点

4.1 现象:启动 Tomcat 时报Invalid bound statement (not found),Mapper 接口找不到对应的 SQL 语句

原因分析:Mapper 接口和 XML 文件没有放到同一个包路径下,或者 MyBatis 的 mapper-locations 配置没有指向 XML 所在目录。这是所有 SSM 新手第一个遇到的报错,本质是「接口编译成 class 后,运行时去 resources 里找 XML,按全限定名找,路径对不上就找不到」。

解决方案:一种做法是把 XML 和接口放在同一个包路径下,比如接口在com.example.dao.ProductMapper,XML 也放在com/example/dao/目录下。但 maven 默认编译时不会把 src/main/java 下的 XML 一起编译输出,需要修改 pom.xml 加一段资源配置,或者干脆把 XML 放到 src/main/resources/mapper 目录,然后在 Spring 配置里指定 mapper-locations 为classpath:mapper/*.xml。我习惯用后者,因为 resources 本来就是放资源文件的地方,不用改 pom。检查你的 spring-mybatis.xml 里是不是写着<property name="mapperLocations" value="classpath:mapper/*.xml"/>,再确认 XML 里的 namespace 是不是等于接口的全限定名。

4.2 现象:数据库连接报Public Key Retrieval is not allowed,或者时区错误The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized

原因分析:你用的是 MySQL 8.0 及以上版本,但 JDBC 驱动连接串里没有加 allowPublicKeyRetrieval 参数,或者没有设置 serverTimezone。MySQL 8.0 默认的认证插件是 caching_sha2_password,换一种说法是它要求客户端在首次连接时安全地获取公钥,而你的驱动没允许它这么做。

解决方案:改 jdbc.properties 连接串,把参数补全。注意 MySQL 8.0 要配 com.mysql.cj.jdbc.Driver 而不是旧版的 com.mysql.jdbc.Driver,如果驱动版本是 5.x,直接换成 mysql-connector-java 8.0.x。连接串写法是:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码

4.3 现象:数据库建表成功,但中文插入或显示乱码

原因分析:三层字符集没统一。数据库连接串里没指定 characterEncoding=utf8,表本身的字符集是 latin1,或者 JSP 页面没设置 pageEncoding。三个环节任何一层不是 utf8,中文就乱码。

解决方案:连接串加characterEncoding=utf8,建表语句里显式写DEFAULT CHARSET=utf8mb4(MySQL 8.0 推荐 utf8mb4,能存 emoji 表情),JSP 页面头部加<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。如果已经建表了,用ALTER TABLE t_product CONVERT TO CHARACTER SET utf8mb4;还可以救回来。这个顺序不要做反,先改连接串再改表,最后刷新页面。

4.4 现象:Tomcat 正常启动,但点任何请求都返回 404,页面找不到资源

原因分析:SpringMVC 配置里把静态资源给拦截了,或者说 Controller 的扫描路径和 JSP 在 WEB-INF 下的实际路径不一致。常见的坑是配置<url-pattern>/</url-pattern>时没有配置<mvc:default-servlet-handler/>,导致 CSS、JS、图片全部被 DispatcherServlet 拦住。

解决方案:spring-mvc.xml 里加上<mvc:default-servlet-handler/>和<mvc:resources mapping="/static/**" location="/static/"/>这两行。它相当于把「处理不了的请求」还给容器默认 servlet,静态资源就能正常加载了。

4.5 现象:MySQL 启动报Can't connect to local server through socket '/tmp/mysql.sock'

原因分析:这个报错通常出现在 Linux 环境下,MySQL 服务没有启动,或者 socket 文件路径被移动过。很多同学是跟着教程装了 MySQL 但没设置系统服务开机自启,重启后就变成连不上。

解决方案:先检查服务状态,Ubuntu 用systemctl status mysql,CentOS 用systemctl status mysqld或service mysqld status。如果显示 inactive,执行启动命令后再用mysql -uroot -p登录测试。遇到这个报错最容易造成「明明配置都对但连不上」的错觉,先验系统服务,再验 JDBC 配置,顺序不要搞反。

5. 从源码到可演示项目:一份拿来即用的运行验证清单

5.1 启动顺序与最低可用配置

跑这套系统前,环境最低配置是 JDK 1.8、Maven 3.6+、Tomcat 8.5、MySQL 5.7 或 8.0。MySQL 如果是 5.7,jdbc 驱动改成 5.1.47 版本即可,不用强上 8.0。启动的顺序是 MySQL → Tomcat → 浏览器访问登录页。原工程是 Maven 结构,导入 IDEA 后先执行mvn clean package -DskipTests打成 war 包丢进 webapps 能跑,但调试期建议在 IDEA 里配置 Tomcat 直接 Debug 启动。

# 如果你用命令行打包,在工程根目录执行 mvn clean package -DskipTests # 打包完成后 war 文件在 target 目录下,拷到 Tomcat 的 webapps 目录 cp target/cashier-0.0.1-SNAPSHOT.war /path/to/tomcat/webapps/cashier.war # 启动 Tomcat /path/to/tomcat/bin/startup.sh

5.2 验证清单:从登录到报表,每步都过一遍

启动成功后,按下面这份清单走一遍,每步都对应一个业务功能。这一步的目的不是「能跑就行」,而是帮你理解页面操作背后调用了哪些接口,答辩时被问到「你这个收银系统流程是什么样的」,你就能从页面一路讲到 SQL 层:

步骤操作预期结果背后逻辑
1用 admin 账号登录跳转到收银台主页登录拦截器放行,session 保存用户
2新增一个商品分类「饮料」分类列表出现新条目t_category 的 insert
3在「饮料」下新增商品「可乐」,单价 3.5商品列表中可乐显示分类名称商品表关联分类表查询
4收银台搜索「可」,加入 2 个可乐购物车数量为 2ProductMapper 关键字查询
5点击结算生成订单,状态为待支付OrderServiceImpl.createOrder
6点击「确认收款」订单状态变已支付Status 从 0 变 1
7查看订单列表订单号码、总价 7.00明细表快照查询
8查看营业额报表今日该笔交易计入报表聚合查询 group by hour

这 8 步全部跑通并且能一边点一边说出对应代码的位置,你已经具备独立掌握这套源码的能力。第 5 步可以故意操作一次:把库存改成 1,然后下单 2 件,看系统是否报「库存不足」——这个「失败路径」比「成功路径」更有演示价值,答辩时主动演示一个能拦截异常的操作,分数不会低。

5.3 进阶改造:从「答辩合格」到「面试有加分」

这套源码满足毕设没问题,但如果你想让它变成简历上的亮点,有三处低成本改造,工作量都不大,但对面试杀伤力很大。

第一处是把模拟支付改成「更完善的状态推进」,为订单增加「退款」路径:已支付订单可以发起退款,状态从 1 走到了 4(已退款),并记录退款时间和操作人。这一步改的只是状态常量和 Service 层的逻辑,加上一个 update 语句。但你在简历里写「订单支持退款流程」就比「订单能支付」多了一层业务理解。

第二处是给商品列表查询加分页。现在的查询是ORDER BY id DESC一把梭,你引入 PageHelper 插件,只需要改两行代码:

// 在 Service 方法里,Mapper 查询前加一行 PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.selectByCondition(condition); PageInfo<Product> pageInfo = new PageInfo<>(list);

PageHelper 会自动在 SQL 后面追加 LIMIT 子句,而且不会拦截非查询方法。这是 Spring Boot 项目里最常用的分页方案,提前用熟它,以后做互联网项目能省很多事。

第三处是把报表展示从纯表格改成 ECharts 图表。工程里后端已经返回了每个小时的营业额数据,你在前端引入 ECharts 的 CDN,把 Map 数据喂给折线图组件即可。这一步技术含量不高,但视觉效果极佳,答辩时评委看到图表直接认为你有真实开发经验。

我每次帮人改这类毕设代码,最后都会强制自己走一遍「查依赖 → 查连接串 → 查 XML 扫描路径 → 查状态流」这四步排查法。这套收银系统我拆过很多次,你只要把第 4 章的那五个坑提前标记在笔记本上,跑通它需要的调试时间不会超过一晚上。希望这份拆解能帮你把项目真正变成自己的东西——毕竟答辩时被问到「这段代码为什么这么写」,你得答得上来才行。

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

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

基于机器学习的蛋白质亚细胞定位预测:从序列到分类的完整流程

简介&#xff1a;这份PDF文献面向生物信息学、蛋白质组学方向的学习者与研究者&#xff0c;聚焦机器学习方法在蛋白质亚细胞定位预测中的应用&#xff0c;帮助读者理解如何从蛋白质序列中提取特征并完成多分类预测&#xff0c;适合具备一定机器学习与生物学基础的读者参考。资源…

作者头像 李华
网站建设 2026/9/25 6:28:46

LibreOffice安装与使用全攻略:从桌面办公到服务器自动化转换

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

作者头像 李华
网站建设 2026/9/25 6:26:24

ESP32上WASM为何无法直接调用GPIO等硬件外设

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

作者头像 李华
网站建设 2026/9/25 6:26:21

Excel+USB转I2C适配器:400kHz地址扫描与调试方案

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

作者头像 李华
网站建设 2026/9/25 6:25:17

51单片机驱动24BYJ48步进电机:ULN2003接线、代码与避坑指南

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

作者头像 李华