news 2026/9/28 15:50:51

从零构建Servlet+JDBC点餐系统:MVC分层、事务与连接池实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建Servlet+JDBC点餐系统:MVC分层、事务与连接池实战

简介:这份压缩包是一套基于MVC架构的JavaWeb点餐系统完整项目,适合用作毕业设计、课程设计或Servlet与JDBC入门实战练习。项目从前台点餐到后台管理,覆盖用户注册登录、菜品分类展示、购物车与订单提交、订单管理等功能模块,通过Model层操作数据、View层渲染JSP页面、Controller层调度请求,清晰呈现经典分层思想。包内共139个文件,以81张界面效果截图、20个JSP页面、6个Java源码及对应class文件为主体,同时包含Jar依赖库、SQL数据库脚本和需求文档,可用于还原数据库表结构并辅助二次开发。资源仅3.76MB,便于快速下载,目前已有61人学习使用。配合需求文档和源码阅读,能够理解原生Servlet处理HTTP请求、JDBC连接与操作MySQL、HttpSession会话管理、用户权限校验等关键流程,对完成课程项目或积累Web开发经验有明显帮助。

1. 为什么点餐系统是最适合练Servlet+JDBC的MVC项目

如果你已经写完Java语法,准备往Web方向走,又不想一上来就撞上Spring Boot的大黑匣子,那这个项目会是一个非常合适的中间站——基于MVC开发模式,用原生Servlet做控制器,用JDBC操作MySQL,整个过程不依赖任何框架,你能清楚地看到一次HTTP请求到底经过了哪些代码,Session在哪里生效,事务又是从哪里开始回滚的。点餐系统之所以适合做这个技术栈的载体,是因为它的业务主线足够简单:用户登录、浏览菜品、下单结算、订单查询,四条链路就能把Servlet、JDBC、MVC三层架构串完整。更关键的是,这个项目做完,你会对Tomcat如何加载Servlet、接口如何被调用、SQL事务在哪一层控制这些基础问题有真正的肌肉记忆,而不是靠着Spring的注解活在一个“约定大于配置”的假象里。

2. 用MVC拆点餐系统:先建模,再分层

2.1 点餐系统的核心业务域与表结构

很多人在写MVC项目时先写Controller,再补DAO,最后发现业务逻辑散落在Servlet里,改一个需求要动三个类。正确顺序应该是先做业务建模,把点餐系统里会出现的对象、对象之间的关系理顺,再去想Controller怎么接收请求。点餐系统的核心对象没那么多:用户、菜品、购物车、订单、订单明细。其中购物车是会话级别的临时数据,不需要落库,剩下四张表就构成了最小可用模型。

create table t_user ( user_id int primary key auto_increment, username varchar(50) not null unique, password varchar(100) not null, role tinyint default 0 comment '0-顾客 1-管理员' ); create table t_dish ( dish_id int primary key auto_increment, dish_name varchar(100) not null, price decimal(10,2) not null, stock int not null default 0, category varchar(50), status tinyint default 1 comment '1-上架 0-下架' ); create table t_order ( order_id int primary key auto_increment, user_id int not null, total_price decimal(10,2) not null, status tinyint default 0 comment '0-已提交 1-已接单 2-已完成 3-已取消', create_time datetime default current_timestamp, foreign key (user_id) references t_user(user_id) ); create table t_order_item ( item_id int primary key auto_increment, order_id int not null, dish_id int not null, quantity int not null, unit_price decimal(10,2) not null, foreign key (order_id) references t_order(order_id), foreign key (dish_id) references t_dish(dish_id) );

这里订单项表的作用是“快照”。查询订单时不能直接去关联t_dish拿当前价格,因为菜品价格可能改过,下单时的成交价要保留在order_item的unit_price字段里。凡是涉及金额、数量、状态的字段,优先考虑用数值类型存,不要用varchar存价格,否则后续汇总订单金额时还得做类型转换。join查询时外键约束看着严格,但如果你之后想拆表迁移数据,外键往往是个坑,所以生产环境里很多团队干脆不用外键,靠代码维护一致性。这个项目作为学习用途,建外键能帮助你理解表之间的关系,保留无妨。

2.2 原生Servlet与JDBC在MVC中的职责边界

MVC三层架构的分工,在这个项目里必须非常明确。View指JSP页面,只负责渲染数据,不做业务判断;Controller是Servlet,接收HTTP请求、解析参数、调用业务层、决定跳转到哪个页面;Model包括两个层级,Service层放业务逻辑,DAO层放JDBC增删改查。

最常见的错误是把业务逻辑写在Servlet里,比如下单时直接在doPost里算总价、操作购物车、甚至写SQL。这样做在小项目里能快速跑通,但是一旦需求变化,比如下单逻辑从“只扣库存”变成“扣库存前先检查会员折扣”,你得在多个Servlet里改同一段代码。正确的写法是Servlet只做三件事:拿参数、调Service、根据Service返回结果决定页面去向。Service只做业务编排:先验证库存,再计算总价,最后调用DAO落库。DAO只做SQL映射。

用代码表达这个边界的话,Controller层看起来就应该是干净的薄壳:

@WebServlet("/dish/list") public class DishListServlet extends HttpServlet { private DishService dishService = new DishService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String category = req.getParameter("category"); List<Dish> dishes = dishService.listOnSell(category); req.setAttribute("dishes", dishes); req.getRequestDispatcher("/WEB-INF/views/dish_list.jsp").forward(req, resp); } }

这个Servlet里没有一行SQL,也没有任何if else判断菜品状态,只是把请求转发给Service层处理。Service返回null或者空列表的处理也交给JSP里判断。为什么要这样?因为Java Web项目的调试成本主要集中在业务规则上,而业务规则放得越集中,出问题时定位越快。你在Service里看到调用链路,在DAO里看到SQL,在Servlet里看到跳转,这是最容易被新手接受的分层方式。

2.3 工程目录:一个能直接跑起来的包结构

经过验证,合理的包结构是下面这样。注意它跟Maven的约定不完全一样,但用于手动管理的Dynamic Web Project完全没有问题:

diancan/ ├── src/ │ ├── com/diancan/controller/ // Servlet类 │ ├── com/diancan/service/ // 业务接口 + 实现类 │ ├── com/diancan/dao/ // 数据访问接口 + 实现类 │ ├── com/diancan/entity/ // 实体类 User, Dish, Order │ ├── com/diancan/util/ // JDBC连接工具类 │ └── com/diancan/filter/ // 字符编码过滤器 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ ├── lib/ // 放mysql-connector-java.jar等 │ │ └── views/ // JSP文件放这里 │ ├── index.jsp │ ├── css/ │ └── js/

把JSP放在WEB-INF/views下是一个值得养成的习惯,因为WEB-INF目录下的资源Tomcat不直接对外提供访问,所有JSP都必须走Servlet转发才能渲染。这意味着像dish_list.jsp这样的页面只能通过/dish/list这个URL进入,用户跳过了登录直接访问页面路径会得到404,这就自然地约束了访问入口。很多老项目把JSP直接扔在WebContent根目录,输入URL能直接打开JSP,往往会出现绕过登录、绕过权限校验访问页面的漏洞。

3. JDBC数据库访问层:从连接管理到增删改查的完整实现

3.1 JDBC连接工具类:驱动加载与Connection管理

写JDBC增删改查之前,得先把连接问题解决好。原生JDBC获取连接的常规方式是通过DriverManager,每次调用getConnection都是从数据库新建一个物理连接。在低并发学习项目中这是没有问题的,但有一点必须注意:驱动加载要放在静态代码块里,保证整个应用生命周期内只执行一次。

package com.diancan.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JDBCUtils { private static final String URL = "jdbc:mysql://127.0.0.1:3306/diancan" + "?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("找不到JDBC驱动,请检查mysql-connector-java是否加入lib目录"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement st, java.sql.ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (st != null) { try { st.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

关连接的顺序是ResultSet先关、Statement再关、Connection最后关,这跟资源的依赖关系有关。实际开发中你很少手动写这段close逻辑,但是在这里必须理解——连接不是用完就消失的,它是被归还给数据库的物理资源,不及时close会导致数据库连接耗尽,服务几小时后突然变得极慢。URL参数里的characterEncoding=utf8决定的是客户端发给MySQL的字符编码,如果这里漏掉,中文菜名写入数据库后极大概率变成问号。serverTimezone参数是MySQL 8.x驱动必须指定的,否则会报时区错误,这也是热词里“this version of the jdbc driver is only compatible with”一类报错的最常见来源——不光版本要匹配,时区参数也容易漏。

3.2 用PreparedStatement完成菜品查询,防SQL注入

查询菜品列表是点餐中最频繁的操作。用PreparedStatement而不用Statement,不是风格偏好,而是安全问题。SQL拼接字符串带来的注入风险在Web项目里是被反复验证过的血泪教训。

public List<Dish> findByCategoryAndOnSell(String category) throws SQLException { String sql = "select dish_id, dish_name, price, stock, category from t_dish " + "where category = ? and status = 1 order by dish_id"; List<Dish> list = new ArrayList<>(); try (Connection conn = JDBCUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, category); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dish dish = new Dish(); dish.setDishId(rs.getInt("dish_id")); dish.setDishName(rs.getString("dish_name")); dish.setPrice(rs.getBigDecimal("price")); dish.setStock(rs.getInt("stock")); dish.setCategory(rs.getString("category")); list.add(dish); } } } return list; }

PreparedStatement的占位符相当于把变量做参数化处理,数据库端先编译SQL结构,再填充值,输入的内容永远只能作为数据出现,不能改变SQL语法。还有一层好处是性能:同样的SQL语句只要数据库缓存没失效,就能复用执行计划,要比每回重新拼接SQL快一些。Java 7之后可以用try-with-resources自动关闭连接对象,这里conn、ps、rs都实现了AutoCloseable接口,代码比原来try/catch/finally的写法短很多。但是注意,事务场景不能这么用——事务要求多个SQL操作共用同一个Connection,如果你在DAO的每个方法里都开新连接,事务就无从谈起。这就是下一个问题的关键背景。

3.3 下单事务:一个请求里的多条SQL必须共用一个连接

下单这个动作会同时往t_order插入一条订单,往t_order_item插入若干条明细,还要扣减t_dish的库存。这三步任何一步失败,之前成功的写入都要撤销。MVC的Service层拿到用户购物车Map(key是dishId,value是购买数量),逐项计算金额,然后执行三条SQL。

public void placeOrder(int userId, Map<Integer, Integer> cart) { Connection conn = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); BigDecimal total = BigDecimal.ZERO; for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { Dish dish = dishDao.findById(entry.getKey(), conn); int quantity = entry.getValue(); if (dish == null || dish.getStock() < quantity) { throw new RuntimeException("菜品库存不足: " + entry.getKey()); } total = total.add(dish.getPrice().multiply(BigDecimal.valueOf(quantity))); } long orderId = orderDao.insertOrder(userId, total, conn); for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { int dishId = entry.getKey(); int quantity = entry.getValue(); Dish dish = dishDao.findById(dishId, conn); orderItemDao.insertOrderItem(orderId, dishId, quantity, dish.getPrice(), conn); dishDao.deductStock(dishId, quantity, conn); } conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("下单失败,已回滚", e); } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这段代码的关键在于所有DAO方法都接收一个conn参数。很多项目早期版本里DAO内部自己调JDBCUtils.getConnection,各写各的,下单到一半抛异常,前面的insert已经提交了,后面的明细没写进去,订单表就会多出一条没有明细的挂单数据。把Connection当作参数从Service传到DAO,事务边界就控制在了Service层——Service里setAutoCommit(false)开始事务,全部DAO操作完成后commit,任何一个环节异常就rollback。这里不用try-with-resources的原因也是因为Connection要在catch块里做rollback,在finally里做close,资源关闭时机没法被try-with-resources的隐式关闭完全覆盖。

4. Servlet控制器层:请求如何变成页面跳转

4.1 Servlet生命周期对控制器代码的约束

Servlet从加载到卸载要经历init、service、destroy三个关键阶段。init只执行一次,适合做重资源初始化;service每次请求都会执行,service内部再根据HTTP方法分派到doGet或doPost。这个生命周期直接决定你的控制器代码能写什么不能写什么。

最常见的两个错误:一是在每个doGet里new一个Service,表面上没问题,但Servlet默认是单例多线程的,同一个Servlet实例会被多个请求线程并发复用,每次new出来的Service对象用完就丢弃,没必要;二是把Connection做成了Servlet的成员变量,这就很危险了——Servlet是多线程共享的,如果多个用户同时下单,请求A的Connection可能正被请求B使用,事务边界会完全混乱。正确做法是Servlet里只放无状态的Service对象,Connection放在Service方法的局部变量里,每个请求拿自己的连接。关于Servlet的线程安全问题,再补一个点:不要在Servlet里用成员变量保存当前登录用户。用户信息放在HttpSession里才是安全的,因为每个请求线程从自己的Session中取出独立的用户对象。

4.2 用原生Servlet实现登录、菜品展示与提交订单

登录功能是Servlet的入门必修课。doPost里拿到用户名和密码,调UserService查库验证,成功就把User对象塞进Session,失败则把错误信息塞进request域然后转发回登录页。注意用forward还是sendRedirect是有讲究的,见代码后的说明。

@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/dish/list"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

req.setCharacterEncoding("UTF-8")这一行极其重要,如果漏掉,从表单提交过来的中文参数在request里已经是乱码了,后面无论怎么改数据库编码都救不回来。登录成功后使用sendRedirect,会让浏览器发起一个新的GET请求到/dish/list,页面可以正常刷新,后退也不会触发表单重复提交。登录失败时用forward,因为要携带errorMsg到JSP渲染,而forward是服务器内部的跳转,request域里的属性不会丢失。两种跳转方式在Servlet里都是日常操作,把它们用对场景,你的控制器才算写得到位。

展示菜品列表用doGet,从request里取category参数,调Service查询,最后转发到JSP。JSP里用c:forEach遍历dishList,就是标准的MVC里View的工作。提一句:JSP页面中不要出现<% %>里写大段Java代码的情况,整个页面的脚本片段应该只用于遍历数据和输出表达式。

4.3 转发与重定向:何时选哪个

转发与重定向的选择困扰过每一个刚学Servlet的人,这里直接用表格理清:

对比维度forward转发sendRedirect重定向
发生位置服务器内部服务器通知浏览器发新请求
地址栏变化不变变为目标URL
request域属性可用丢失
执行次数一次请求两次请求
适用场景携带数据回显列表页、登录失败返回提示登录成功、下单成功、表单提交完成

还有一点,如果用户在订单提交页面反复刷新,每次都执行一次insert,购物车会被重复下单。所以POST请求处理成功后必须用redirect,让刷新动作落在GET请求上,而不是再次触发POST下单逻辑。这是Web开发里最经典的防重复提交方案:Post/Redirect/Get。下单成功后跳转到/order/detail?orderId=xxx,刷新页面只是重新查询订单详情,不会重复插入订单。

5. 避坑清单:原生Servlet+JDBC最容易翻车的五个现场

5.1 IDEA里写了Servlet代码,一部署就404

现象是不报编译错误,运行Tomcat后输入地址却提示HTTP状态404。原因有两个方向:一是Servlet类上根本没有映射URL,不管是用@WebServlet注解还是web.xml里的servlet-mapping,漏了任何一个都进不到目标方法;二是项目部署到Tomcat时,Tomcat加载的是WebContent目录下的class文件,如果你改了代码没重新构建,或IDEA的Artifact没把最新的classes同步进去,就会出现“代码明明改了但行为没变”的错觉。解决路径是先打开Tomcat的控制台日志看启动时有没有打印Servlet的初始化信息,再确认访问的URL和注解里的值是否完全一致,包括大小写和斜杠。最后在IDEA里执行Build -> Rebuild Project,重新部署一次。

5.2 JDBC驱动加载不出来:Maven自动下载失败或驱动类找不到

现象是启动Tomcat时点击任何涉及数据库的功能,后台抛ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因一般是mysql-connector-java.jar没有真的在你的WEB-INF/lib目录下。热词里提到的“intellij idea sqlserver jdbc 自动下载 download from maven failed”是同类问题——IDE尝试从Maven中央仓库下载驱动,但网络环境访问不了仓库。解决方法是手动到Maven仓库或者官方下载页面获取jar包,直接复制到WEB-INF/lib下,在IDEA里右键Add as Library。同时检查你引入的是哪种驱动:MySQL 5.x驱动类名是com.mysql.jdbc.Driver,MySQL 8.x是com.mysql.cj.jdbc.Driver,驱动和版本不匹配时会报各种诡异连接错误。在pom.xml或者lib目录里只保留一个版本的驱动文件,避免冲突。

5.3 中文菜名全部变成问号

现象是页面上显示菜品名称正常,但写入数据库后变成“???”。原因是字符集链的某一环断了。这条链包括:JSP页面的pageEncoding、Tomcat接收POST请求的编码、JDBC URL里的characterEncoding参数、数据库表和字段的字符集。在MySQL侧的检查SQL是show variables like 'character_set%';,确保database和server都是utf8mb4。JDBC URL里要带上useUnicode=true&characterEncoding=utf8,缺一不可。最后在项目里加一个Filter统一设置请求编码是彻底的解决办法,后面的第6章会给出代码,这里先把滤波器的概念放到这里:Filter的doFilter方法在Servlet执行前拦截,filterChain.doFilter放行后Servlet再拿到的request参数就已经是UTF-8了。

5.4 下单成功但订单表里没有数据

现象是页面上提示“下单成功”,跳转到订单详情也没问题,但数据库t_order表里就是没有记录。原因非常典型:你用的连接没有开启事务,或者开启了事务但没有commit。MySQL的InnoDB引擎在autocommit=true时,每一条insert都会自动提交,如果代码里setAutoCommit(false)之后没有在任何地方调用commit,事务一直悬着,连接一关,这个未提交事务就回滚掉了。另一类是insert语句执行了,数据库也确实有数据,但Navicat里没刷新,这类误判也经常发生。解决方法是下单的Service里确认commit一定在整个业务链路全部执行之后再调用,同时检查DAO层是不是在用同一个conn,不要DAO内部开新连接把事务切断。

5.5 运行几小时后数据库连接耗尽,服务失去响应

现象是Tomcat没有崩,内存也没异常,但任何涉及数据库的请求都卡死。原因是Connection一直没关,最后数据库的max_connections被占满。排查时你会在数据库端看到大量Sleep状态的连接,时间越堆越长。这一类问题的源头几乎都是代码里的资源关闭没有放在finally块里。有一个容易被忽略的细节:PreparedStatement.close()不会自动关闭底层的Connection,如果你只想关闭Statement而保留连接,可以在finally区分资源类型。点餐系统的正确做法是每一个DAO方法遵循“用完即关”的原则,Connection在方法里创建,在方法里销毁;事务场景则让Service在finally里统一关闭这个共享的Connection。贴上JDBCUtils.close方法的意义就在于此,没有兜底销毁逻辑,数据库连接池就会在你看不见的地方慢慢被拖垮。

6. 让点餐系统扛住真实请求:三个值得做的改进

项目跑通之后,给代码做三个升级,难度不高但收益立竿见影。第一个是用Druid或HikariCP连接池替代DriverManager。原生DriverManager每次getConnection都会走一遍TCP握手,高并发下这是巨大的开销,连接池的作用是维护一批现成的连接复用。以Druid为例,用Properties文件配置initialSize=5、maxActive=20,在静态代码块里初始化DruidDataSource,后续DAO里拿连接改成dataSource.getConnection()。注意这里改的是JDBCUtils的getConnection方法,DAO和Service层的代码结构完全不用动,MVC分层的好处之一就在这里体现。第二个改进是为菜品列表做内存缓存:在DishService里用一个定时刷新的Map保存菜品列表,每次请求先查缓存,没有命中再走JDBC。要注意缓存更新策略,最简单的方式是菜品上下架操作时主动清空缓存,让下一次请求重新加载。第三个是字符编码Filter,用javax.servlet.Filter接口写一个编码过滤器,在doFilter方法里setCharacterEncoding("UTF-8"),然后chain.doFilter。这样的话,项目里所有Servlet都不用再重复写req.setCharacterEncoding这一行。

@WebFilter("/*") public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); chain.doFilter(req, resp); } }

这三个改进分别对应资源复用、响应速度和编码整治。我当时在点餐系统里补上连接池后,用JMeter模拟了50个并发用户同时下单,数据库连接数从频繁波动变成了稳定在10条左右,页面响应时间也明显回落,那一刻才真正理解“连接复用”在实际部署中的意义。如果你也在做这个项目,建议跑完基本链路就把这三个改造做掉,你会发现同一个代码结构,经过一轮优化之后你能说清楚的东西多了很多。希望帮到你。

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

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

Jev模型源码解析:不生成文字的轻量级单token预测器

前两天我在 Hacker News 上刷到一个节奏感很强的项目&#xff1a;发布 3 天&#xff0c;直接登顶首页第一&#xff0c;标题写着“不生成一个字的模型”。我本来以为又是那种噱头拉满的 AI 玩具&#xff0c;点进 GitHub 之后反而越看越上头。Jev 这个项目和我想象的不太一样&…

作者头像 李华
网站建设 2026/9/28 15:50:33

容器冷启动优化:Agent服务快照恢复实战,从35秒到1秒

我最近被一个 Agent 服务的冷启动坑得够呛。团队把一个大模型 Agent 框架打包进容器&#xff0c;加上 Python 依赖、几个本地 embedding 模型文件&#xff0c;镜像轻松超过 1.5GB。每次弹性扩容或发布新版本&#xff0c;新容器要经历拉镜像、解压、初始化框架、加载模型这一整套…

作者头像 李华
网站建设 2026/9/28 15:50:05

RK628F MIPI转HDMI黑屏排查实战:从I2C到固件到4K时序

最近在调一块RK3588方案的板卡&#xff0c;外接的显示输出就是一颗RK628F桥接芯片&#xff0c;作用是把SoC的MIPI DSI输出转成HDMI&#xff0c;接到4K显示器上。从拿到样板到屏幕真正点亮&#xff0c;中间黑屏了将近一周。这类方案在初期出现黑屏太正常了——RK628F不是你焊上去…

作者头像 李华
网站建设 2026/9/28 15:49:26

舌苔识别检测系统:基于深度学习的细粒度分类与GUI实现

简介&#xff1a;一套基于深度学习的舌苔识别检测鉴定系统&#xff0c;面向计算机相关专业正在准备毕业设计的学生&#xff0c;也适合需要项目实战练习的学习者&#xff0c;可作为毕业设计、课程设计或期末大作业。资源提供完整的Python源码、论文文档和GUI界面&#xff0c;覆盖…

作者头像 李华
网站建设 2026/9/28 15:49:03

魔百盒M301H/UNT401H刷机指南:芯片版本与固件匹配避坑

先交代一下背景&#xff0c;可能很多朋友和我一样&#xff0c;手里都有一台移动宽带送的“魔百盒”。这玩意儿在运营商那儿是正经IPTV盒子&#xff0c;但在我们玩机的人眼里&#xff0c;它就是一台配置尚可的安卓设备。问题在于&#xff0c;魔百盒的系统被移动和代工厂深度定制…

作者头像 李华
网站建设 2026/9/28 15:48:12

恒科超声波焊接设备实力如何

顺应制造升级浪潮 锚定工业清洗赛道使命 把握产业转型需求 明晰品牌发展定位国内制造业已经进入高质量发展的新阶段&#xff0c;零部件加工精度不断提升&#xff0c;下游成品对核心构件的洁净度要求持续提高&#xff0c;清洗工序作为零部件加工的关键后置环节&#xff0c;直接影…

作者头像 李华