news 2026/9/10 18:35:28

基于SSM框架的在线影评网站开发实践与部署指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM框架的在线影评网站开发实践与部署指南

1. 项目整体设计与技术选型:为什么用SSM做影评站

1.1 项目背景与核心需求

做在线影评网站,说难不难,说简单也真不简单。我接到这个项目的时候,甲方需求很明确:要一个能展示电影信息、用户能登录后写影评、打分、收藏电影的完整平台,管理员还要能管理电影分类、审核评论、查看用户情况。说白了,就是一个典型的内容管理系统加上用户交互模块。这种需求在课程设计、毕业设计甚至一些小企业的内部项目中都非常常见,所以我把整个开发过程整理出来,给正在做同类项目的朋友一个参考。

从标题就能看到,这个项目走的是SSM技术栈,也就是Spring、Spring MVC、MyBatis三件套。这套组合在Java Web开发里属于经典中的经典,虽然现在Spring Boot已经很流行了,但SSM能帮你把底层原理吃得更透——手动配置事务、手动管理SqlSession、手动写XML映射文件,每一次配置都是一次对框架机制的复习。实际跑下来,我对Spring IOC容器、AOP切面、MyBatis动态SQL的理解,比之前光看教程要深刻得多。

这类影评平台的核心里面有两条线:一条是用户看到的,包括注册登录、浏览电影、查看详情、写影评、打分;另一条是管理员看到的,包括电影信息维护、评论审核删除、用户管理、数据概览。两套逻辑都要落地到同一个数据库里,而SSM恰好能把这种分层的业务逻辑处理得很干净。Controller接收请求、Service处理业务、Mapper操作数据库,职责清晰,调起Bug来也好定位。

1.2 技术选型:SSM框架组合的理由

选SSM而不用更时髦的Spring Boot,很多人觉得是老古董,但做这个项目恰恰需要这种"老古董"带来的掌控感。Spring Boot自动配置虽然省事,但遇到问题的时候,你很难说清楚数据源是怎么初始化的、事务是怎么代理的。而在SSM里,一切都是显式的——spring-mvc.xml、spring-mybatis.xml、web.xml三个配置文件把整个应用的骨架撑起来,每个Bean的创建、每个切面的织入点、每个Mapper的扫描路径,全都清清楚楚。

Spring在SSM里管什么?管对象。所有Service、Controller、DAO的实例都由Spring容器统一创建和管理,用到哪个就注入哪个。Spring MVC管什么?管请求路由和页面跳转。前端发来的HTTP请求,由DispatcherServlet分发到对应Controller方法,处理完再返回视图。MyBatis管什么?管数据库操作。写SQL查询,再把结果集自动映射成Java对象。

举个例子,用户登录这个最简单的功能,在SSM里的完整链路是这样的:浏览器提交用户名密码→DispatcherServlet拦截到请求→根据@RequestMapping找到LoginController的login方法→方法调用UserService的login方法→UserService调UserMapper的selectByUsername方法→MyBatis执行XML里写的SQL语句→结果逐层返回,最终视图渲染出登录成功或失败的页面。这条链路环环相扣,每一步都有明确的类和方法对应,排查问题的时候能顺着调用栈一路看下去,这就是SSM最大的价值——结构清晰,可追溯性强。

2. 数据库设计与核心表结构解析

2.1 实体关系梳理

影评平台的数据模型,核心实体其实就那几个:用户、电影、影评、电影分类,再加上收藏关系。实体之间最关键的关联是:一个用户可以写多条影评,一条影评只属于一个用户;一个电影可以有多条影评,一条影评只针对一个电影;一个用户还可以收藏多部电影。这就是典型的一对多和多对多关系,落到数据库设计上,我们需要建五张表,其中收藏关系通过中间表来实现。

我当时先用ER图把所有实体和关系画出来,再转成表结构。这个习惯强烈推荐保留——直接上手建表很容易漏掉外键关系,后面写关联查询时才发现缺字段,返工成本特别高。画ER图的工具用免费的即可,我平时用draw.io,拖拽几下就能完成,关键是思路清晰之后再动手。

设计原则就一条:宁可多建一张表,也不要把所有数据塞在一张表里。比如电影和分类,如果直接把分类名写在电影表里,那以后改个分类名就要批量UPDATE所有电影记录,数据冗余和一致性问题会把人折磨疯。拆成两张表,电影表存category_id,分类表存分类名,用外键关联,问题自然解决。

2.2 核心表字段设计要点

先说说用户表(t_user),字段基本就是老几样:id、username、password、nickname、avatar、role、create_time。特别注意两点:第一,password存的是MD5加密后的密文,绝对不允许明文入库,一旦数据库泄露,用户在所有网站上的密码都会遭殃,这个底线不能破;第二,role字段我用int类型,0表示普通用户,1表示管理员,判断权限时直接比较数值,比字符串效率高,也不会出现大小写不一致的问题。

电影表(t_movie)是信息量最大的表,字段包括id、title、cover_url、director、actors、category_id、description、release_date、avg_score、status。这里的avg_score字段需要单独说一下——很多新手会把评分直接算出来存到这里,但更好的做法是先建一张影评表,每次用户打分时实时计算平均值,再UPDATE到avg_score字段。这样页面展示排行时不需要跑聚合函数,直接按avg_score排序就行,响应速度快很多。更新时机就选在用户提交影评之后,事务里同时插入影评记录并更新电影平均分,保证数据一致性。

影评表(t_review)是整个平台最核心的业务表,字段有id、movie_id、user_id、content、rating、like_count、status、create_time。status字段用来做评论审核,0待审核、1已通过、2已驳回,管理员权限就在这里体现。为什么需要审核?因为影评是公开内容,如果不做任何过滤,垃圾广告、敏感词、剧透内容会直接展示给所有用户,体验极差。我见过不少项目偷懒不做审核,结果上线没几天就被恶意灌水搞崩了,所以这个字段最少要留出来。

收藏表用关系型中间表,t_favorite,字段只有id、user_id、movie_id、create_time,联合唯一约束加在(user_id, movie_id)上,防止重复收藏。五张表之间的关系在MyBatis映射文件里通过resultMap配置关联查询,一对多、多对一都能处理得很顺畅。关于索引,我建议在t_review表的movie_id和user_id上建联合索引,因为这是高频查询条件;在t_movie表的avg_score上建普通索引,排行查询会快很多。数据量少的时候感觉不出来,但一旦影评数据过万,有无索引的查询速度差别是数量级的。

3. 核心功能模块的实现细节

3.1 用户注册登录与会话管理

用户模块是整套系统的大门,体验好坏直接影响用户留存。注册流程我用的是经典三步:校验用户名是否重复、MD5加密密码、插入用户记录。校验和插入之间理论上存在并发问题,同时注册同一个用户名可能双双通过校验,解决办法是在设计表的时候给username加唯一索引,数据库层面兜底,比代码层面判断可靠得多。

登录这块我用HttpSession来管理用户状态。用户登录成功之后,把用户ID和用户名塞进session,后续每个Controller方法里需要判断是否登录时,直接从session里取,取不到就跳转到登录页。为了让代码更优雅,我写了一个自定义拦截器(HandlerInterceptor),在preHandle方法里统一做登录校验,白名单放行登录页、注册页、电影列表等不需要登录就能访问的路径,其他请求一律拦截。这样每个Controller方法里就不用重复写session判空逻辑了。

这里有个小坑值得提醒:session的有效期和浏览器关闭策略密切相关,默认30分钟无操作会被服务器回收。如果用户写了半天的影评提交时才发现session过期,那种挫败感足以让他卸载你的应用。我采用了一种比较简单的解决方案——前端用Ajax统一捕获所有返回401或302且跳转逻辑异常的响应,弹出"登录状态已过期,请重新登录",确认后带着当前页面URL跳转登录页,登录成功后再跳回来。虽然不能解决根本问题,但能大幅度减少用户的无谓操作。

3.2 电影展示与分页检索

电影列表页是所有用户访问量最大的页面,性能优化显得格外重要。我用MyBatis的分页插件PageHelper,一行代码就能实现物理分页:PageHelper.startPage(pageNum, pageSize)放在Mapper查询方法之前即可,插件会自动拦截接下来的SQL并追加LIMIT子句,同时返回一个Page对象,里面有total、pageNum、pageSize等分页信息。

检索功能支持三种维度:按电影标题模糊搜索、按分类筛选、按评分排序。MyBatis的动态SQL在这里发挥了大作用,比如模糊搜索的SQL可以写成这样:

<select id="selectMoviesByCondition" resultMap="BaseResultMap"> SELECT * FROM t_movie <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> <if test="sortField != null and sortField != ''"> ORDER BY ${sortField} DESC </if> </select>

<where>标签会自动处理首条条件前面的"AND"关键字,<if>标签按需拼接条件,既灵活又安全。特别注意排序字段用${}而不是#{},因为排序字段是表字段名,不能被预编译参数占位符替代,但这样也就引入了SQL注入风险——绝对不能让用户自由传入字段名,必须是后端固定枚举值做白名单校验后再拼接。

电影详情页要展示的信息很密集:基本信息、简介、平均分、所有影评列表。这里需要做表关联查询,一条SQL同时查出电影信息和分类名,影评部分单独查出来循环展示。关于封面图片,我建议把图片文件上传到服务器指定目录,数据库只存URL路径,这样的好处是数据库体积不会越来越大,图片加载也可以通过nginx做静态资源加速,不用走Tomcat的应用层。

3.3 影评发布与评分机制

写影评是这个平台最重要的用户行为,因为它同时涉及影评表插入和电影评分更新。我把它放在一个事务里,加上@Transactional注解,任何一步抛出异常都会整体回滚。

@Transactional public void addReview(Review review) { // 1. 插入影评记录 reviewMapper.insert(review); // 2. 重新计算这部电影的平均分 Double avgScore = reviewMapper.selectAvgScoreByMovieId(review.getMovieId()); Movie movie = new Movie(); movie.setId(review.getMovieId()); movie.setAvgScore(avgScore); movieMapper.updateScore(movie); }

评分机制这里有一个设计细节需要说明:我用的是1到10分的整数评分,滑动条控件在前端展示,用户选择后显示对应星标。为什么不用经典的5星制?因为10分制契合国内多数影评平台(豆瓣也是十分制),用户对分数的感知更精确,评分区分度更高,不会出现一群电影扎堆在8分以上的情况。当然这个完全看产品需求,如果甲方明确要五星制,改一下前端展示和评分范围校验就行,后端逻辑不受影响。

影评列表展示时,除了内容正文,还可以显示每条影评的点赞数。点赞功能我用了一张独立的like表,用户点过赞之后状态码从0变1,变回0表示取消。这里最需要注意的是防止重复点赞,我通过数据库唯一约束(user_id, review_id)来保证同一用户针对同一条影评只能有一条记录,再用乐观锁配合点赞数更新,避免并发操作时数字丢失更新。

3.4 管理员后台与评论审核

管理员后台是另一个独立的功能模块,权限控制要和前台严格区分。我的方案是:管理员登录后,session里的role值为1,后台的每个请求在拦截器里同时校验是否登录以及role是否为1,不满足就跳转403页面。后台首页展示统计数字卡片,用简单的SELECT COUNT来统计用户总数、电影总数、影评总数,用平均分排行找出评分最高的10部电影,用最近影评列表做动态刷新。

评论审核功能是管理员最频繁的操作。待审核列表把status为0的评论全部查出来,管理员一条条看,觉得没问题就点通过,敏感内容就点驳回。驳回的时候还可以填原因,用户下次登录会看到自己的影评被驳回以及原因反馈,这种提示机制能有效引导用户发言规范。批量操作我用了复选框加批量更新,Vue前端收集所有选中的ID存到数组里,通过Ajax传给后端,后端用foreach拼接动态SQL做批量UPDATE。MySQL默认是允许这种跨多条记录的更新的,只要在JDBC连接URL里加上allowMultiQueries=true就可以,如果用的是MyBatis的foreach标签生成多条UPDATE语句,那就要确保数据库驱动支持批量执行。

4. 开发环境搭建与调试部署全流程

4.1 环境准备与版本选型

这个项目需要的开发环境,说复杂不复杂,说简单也需要稍微准备一下。我列一张表,你照着配就行:

工具/软件版本建议说明
JDK1.8SSM项目的标准配置,太新的JDK版本可能会有兼容问题
IDEIntelliJ IDEA 2022+社区版足够用,别花钱买旗舰版
Maven3.6+依赖管理和项目构建
Tomcat8.5或9.0Servlet容器,注意9.0之后包名有变化
MySQL5.7或8.08.0需要调整驱动包和时区配置
Navicat任意版本图形化操作数据库,也可以用MySQL Workbench

JDK建议用1.8,原因很简单:SSM项目最活跃的年代就是Java 8的时代,网上能搜到的绝大多数技术问答、配置教程、踩坑案例都是基于JDK 8的,遇到问题你能更高效地找到答案。用JDK 11或17不是不行,但Spring老版本、Tomcat老版本、MyBatis老版本是否兼容就得自己排查了,纯属给自己找麻烦。

4.2 从零配置Maven与IDEA项目结构

Maven的核心作用就是管依赖。在pom.xml里引入spring-webmvc、mybatis、mybatis-spring、druid连接池、jstl、jackson-databind这些核心依赖后,Maven会自动把依赖的依赖也拉下来,完全不用你手动去各大网站下载jar包,这一套流程已经非常成熟了。建议在IDEA里设置一下Maven的镜像源(如果用阿里云私服),否则首次加载依赖会慢得让人怀疑人生。

项目结构我习惯用Maven的标准布局,这也是SSM项目的标配:

src ├── main │ ├── java │ │ └── com.example.movie │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ ├── entity │ │ └── interceptor │ ├── resources │ │ ├── mapper │ │ ├── jdbc.properties │ │ ├── spring-mvc.xml │ │ ├── spring-mybatis.xml │ │ └── mybatis-config.xml │ └── webapp │ ├── WEB-INF │ │ ├── web.xml │ │ └── views │ └── static │ ├── css │ ├── js │ └── images └── test └── java

entity层放数据库表对应的JavaBean,mapper层放接口和XML映射文件,service层放业务逻辑接口和实现类,controller层接收请求并返回视图。分层的价值在排错时体现得最明显:页面数据不对,先看Controller有没有传给视图正确参数;参数正确但SQL报错,去Mapper XML里看SQL语句,整个排查链路非常清晰。

配置文件里有一个地方特别容易踩坑,那就是数据库连接信息。Spring在xml里加载jdbc.properties,里面配置driver、url、username、password。MySQL 8.0的数据库驱动换成com.mysql.cj.jdbc.Driver,连接URL必须加serverTimezone=Asia/Shanghai参数,否则会报时区错误。用MySQL 5.7的话,驱动用com.mysql.jdbc.Driver,不加时区参数也能跑起来。这两个版本混着来的话,报错信息能让人一头雾水,排查半天发现只是版本不匹配。

4.3 本地调试与线上部署的完整流程

调试阶段最常用的手段就是断点调试和日志输出。IDEA的Debug模式启动Tomcat后,在Java代码行号旁点一下就能设断点,运行到那一行会自动暂停,然后可以逐行查看变量的值。这个方法排查空指针异常和参数传递问题特别好用。配合log4j2日志框架,在Service层的入口和出口各打一条日志,记录入参和出参,线上出问题的时候翻日志比盲猜效率高太多了。

本地调试通过之后,部署上线就是把项目打成WAR包,放到Tomcat的webapps目录下。在IDEA里选择Build → Build Artifacts → Build,或者用Maven的package命令,会自动在target目录下生成war包。然后把war包丢到服务器的Tomcat/webapps目录,启动Tomcat,它会自动解压并部署。

这里我要重点聊聊部署过程中的一个经典坑——数据库连接失败。本地连的是本地MySQL,服务器连的是服务器的MySQL,两者可能在版本上、字符集上、用户权限上都不一样。部署前一定要在服务器上测试一下应用服务器是否能连通数据库服务器,尤其要注意MySQL 8.0默认的认证插件是caching_sha2_password,老版本的JDBC驱动可能不认识,这时候需要在数据库执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'来修改认证方式,或者换新版的JDBC驱动。

前端静态资源加载不出来也是高发问题。CSS、JS、图片加载不出来的原因,十有八九是项目发布路径不对。如果Tomcat部署后的应用路径是http://xxx:8080/movie/,那静态资源的路径就要写成${pageContext.request.contextPath}/static/css/style.css,绝对不能写绝对路径/static/css/style.css。前者的contextPath会自动拼接上/movie,后者会直接去服务器根目录找,自然就404了。

5. 常见问题与排查技巧实录

5.1 数据库连接与中文乱码问题

第一个高发问题是数据库连接失败,最典型的报错是Access denied for user 'root'@'localhost',原因基本就是账号密码错误或者用户没有远程访问权限。本地连不上检查密码,服务器上连不上检查user表里的host字段是否允许非本机IP访问。另外,防火墙也要排查,MySQL默认端口3306如果被防火墙拦着,怎么连都白搭。

中文乱码是另一个巨恶心的坑。从浏览器看到的是"你好",写进数据库变成了"ä½ å¥½",这一串乱码问题的根源在于字符集不一致。解决方案分三处进行:代码层面,确保项目所有文件都是UTF-8编码,IDEA右下角能看编码格式;连接层面,在JDBC的URL里加上characterEncoding=utf8;数据库层面,建库时指定DEFAULT CHARSET=utf8mb4,utf8mb4比utf8多了生僻字和表情符号的支持,写影评时万一用户用了emoji,utf8mb4能正常存储和显示,utf8会直接报错。

Tomcat层面也要配一下,在server.xml的连接器里加上URIEncoding="UTF-8"属性,否则GET请求带中文参数时会出现乱码。POST请求的中文乱码可以通过在web.xml里配置CharacterEncodingFilter过滤器来解决,设置强制请求和响应都使用UTF-8。这是一个全局过滤器,必须在第一个位置注册,否则可能因为过滤器链顺序问题导致后面的过滤器拿到的是乱码数据。

5.2 前端与后端联调的常见报错

前后端联调时最让人抓狂的就是跨域问题和JSON解析问题。

跨域问题只在前后端完全分离部署时会遇到。如果前端页面跑在http://localhost:8080,后端接口跑在http://localhost:9090,浏览器的同源策略会直接拒绝请求。解决方案分三种:第一,最简单粗暴的,用Nginx做反向代理,把不同端口映射到同一个域名下,前端感觉不到跨域的存在;第二,后端在响应头里加Access-Control-Allow-Origin: *,谁都能跨,但安全性差;第三,用Spring MVC拦截器统一管理跨域配置。我这个项目用的是前后端写在一起的单体架构,所以这一块踩坑不多,如果是前后端分离项目,跨域问题是绕不开的第一座大山。

JSON解析报错最常见的是HttpMessageNotReadableException,大概率是前端传的JSON格式不对或者字段类型匹配不上。比如后端接收的是带Date类型的字段,前端传来的是字符串"2024-05-01",Jackson反序列化时如果不指定格式就会报错。解决办法是在对应字段上加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,或者在Jackson配置里设置全局日期格式。

5.3 部署上线的隐藏雷区与性能优化

部署上线之后还有几个隐藏雷区,我几乎每次做SSM项目都会碰到,在这里统一说一下。

第一个是MySQL连接超时问题。MySQL默认的wait_timeout是8小时,如果应用在8小时内没有访问数据库,连接会被MySQL服务端强制断开。这时候应用第一次访问数据库就拿了一个失效的连接,直接报Communications link failure。解决办法:在Druid连接池配置里把testWhileIdle设为true,timeBetweenEvictionRunsMillis设为60000,让连接池每分钟检测一次空闲连接是否有效,无效就踢掉。也可以直接把MySQL的wait_timeout调大,但应用层面的检测更稳妥,不依赖数据库配置。

第二个是内存溢出问题。Tomcat默认的JVM内存分配比较保守,用户量上来之后容易OOM。这个项目里影评列表如果一次性加载全量数据,很容易把内存撑爆。代码层面要用分页查询限制每次加载的数据量;JVM层面在Tomcat的catalina.sh里调整一下参数,-Xms512m -Xmx1024m -XX:MaxPermSize=256m,根据服务器的物理内存大小合理分配。

第三个是SQL注入问题。SSM框架天然防注入的地方在于MyBatis的#{}是预编译占位符,传入的参数只作为值绑定,不会拼进SQL结构里。但前文提到排序字段用${}的情况,以及使用字符串拼接SQL的场景,都留下了注入漏洞。有个常识性建议:永远不要相信前端传来的任何参数,哪怕它在隐藏域里也不行。排序字段先枚举白名单,再拼接进去;模糊搜索用#{keyword}而不是'%' + ${keyword} + '%'

6. 这份项目资源的完整内容与使用建议

标题里提到这个项目附带"程序+源码+数据库+调试部署+开发环境",实际拿到手之后大概包含这么几块:IDEA里可以直接打开的完整项目源码,数据库的建库建表SQL脚本,以及一份Word格式的项目论文。论文的撰写格式是按照课程设计或毕业设计的标准来的,包含绪论、需求分析、系统设计、数据库设计、系统实现、系统测试这么几大章节,一万字以上,框架搭得很完整,直接拿来改成你自己的语言风格就能用。

这里我必须强调一点——论文一定要自己改,不要直接交原封不动的版本。你拿到手的项目和你的课程要求、学校模板、个人语言习惯都可能有差异,把这些差异补上,论文才算真正属于你自己的。我之前见过不少同学因为偷懒直接交原版导致查重不过、被判定抄袭,最后折腾半天才解决,得不偿失。

调试部署这一块也会配一份操作手册,大概包括:用IDEA导入项目、配置Maven、修改jdbc.properties里的数据库连接信息、导入SQL脚本、启动Tomcat、用浏览器访问系统这些步骤。跟着操作手册走一遍,基本20分钟就能把项目跑起来。需要注意的点是,手册里的连接信息or默认密码不一定是你的环境能用的,改成本地MySQL的用户名和密码即可。

建议的学习路径是:先把项目跑起来,看看到底长什么样;然后顺着一个功能追代码,比如用户写影评,从JSP页面追到Controller再追到Service再追到XML里的SQL;最后再对照论文看系统设计的思路,这样能在最短时间内把SSM框架的完整调用链路吃透。很多人拿到项目就直接改代码交作业,等于把最有价值的源码学习机会浪费掉了。

7. 扩展与迭代的思路

影评平台这种项目,后续想继续迭代的话,可以扩展的方向其实挺多的。从技术角度说,可以引入Redis做缓存,把电影列表和排行榜这种高频读取的热点数据放到Redis里,数据库压力能降一个量级;可以引入Elasticsearch做全文检索,现在的SQL模糊搜索在数据量过万后性能会明显下降,换成ES的倒排索引,搜索响应速度那是质的飞跃;可以引入消息队列做影评的异步审核,用户提交影评后立刻返回成功,审核结果通过回调通知用户,体验更流畅。

从产品角度说,可以增加用户的个人主页,展示他写过的所有影评、收藏过的电影、获得的点赞数;可以增加电影的推荐系统,基于用户的历史评分做协同过滤,给他推荐可能感兴趣的电影;可以增加影评的点赞评论功能,让用户之间能互动起来;可以增加热门话题或者榜单功能,做运营活动。

就算完全不做技术扩展,把这个项目重写一遍Spring Boot版本也是一个很好的练习。你会发现有Spring Boot帮你搞定一堆自动配置之后,整个项目的代码量能少三分之一,唯一要操心的是业务逻辑本身。先把SSM版本吃透,再上手Spring Boot,你会理解Spring Boot到底自动化了哪些东西,而不是只会无脑套模板,我之前就是这么一步步学过来的。

最后再分享一个小技巧:给这个项目写测试数据的时候,不要用"abc123"或者"测试"这种毫无意义的数据,去真实影评网站扒一批真实电影的信息和影评进去,整个系统的展示效果完全不一样。真实数据能让你更直观地发现分页长度、列表样式、评分排序这些地方存在的问题,我用这个方法找出过不止三五个当时觉得"差不多行了"的UI细节问题。

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

Spring Boot物品捎带平台开发实战:状态机与并发控制

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

作者头像 李华
网站建设 2026/9/10 18:31:27

充电桩管理系统全解析:从OCPP协议到计费运维的实战指南

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

作者头像 李华