news 2026/10/17 9:02:02

快递e栈:Servlet+Filter+MVC全流程JavaWeb项目实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
快递e栈:Servlet+Filter+MVC全流程JavaWeb项目实战

简介:一套用于JavaWeb综合能力训练的项目实践资料,以快递e栈为业务主线,覆盖Servlet、Filter、Session、MVC分层等后端核心,并融入前端页面布局、jQuery与Layui交互组件、Ajax异步请求、MySQL数据存储、Tomcat部署及公有云上线等环节,适合正在做课程设计或综合复习的初、中级开发者。压缩包共831个文件,总体积17.23MB,类型包含Java源码和已编译class字节码、JSP页面、HTML/JS/CSS前端资源、Jar依赖库、XML与properties配置,以及大量PNG/GIF图片素材、PSD设计原稿和字体图标文件,其中PNG和GIF多用于界面展示与操作示意,java和class对应后端源码与编译产物,jar封装第三方依赖,整体目录结构较完整,便于按模块对照阅读。目前已有886人学习下载。借助该资源,学习者可以完整查看前后端交互、数据库读写与会话控制等实现细节,理解过滤器、监听器在真实项目中的注册与调用方式,也能掌握MVC各层的职责划分;同时可在本机或公有云服务器上尝试部署运行,作为课程设计或面试展示的参考项目。

1. 快递e栈能做什么:一个覆盖登录、存取、查询的JavaWeb综合项目

去年我帮某高校快递代收点整理信息流程时,看到工作人员还在翻Excel表格记录每个包裹单号,学生靠报手机尾号找件,取错件、漏件的情况时有发生。于是我把这个场景整个做成了可运行的JavaWeb综合项目——快递e栈。它不依赖Spring全家桶,底层是原生Servlet + JSP + JDBC + MySQL,按MVC思想拆包,把登录注册、快递入库、取件、历史查询、网页端呼号通知串成一条完整链路。对刚入门JavaWeb的开发者来说,它能让你亲眼看到Servlet、Filter、Session、MVC四项技术如何在一个真实项目里互相配合;对已经工作过一段时间的开发者,则能找到过滤器链、会话保持、数据一致性问题这些可复用的工程细节。

2. 整体架构剖析:Servlet、Filter与MVC三层如何协作

项目没有引入微服务,也没有把逻辑塞进几十个互相跳转的JSP页面。请求从浏览器出发,经过Tomcat容器,先被过滤器链统一处理,再进入对应的Servlet;Servlet负责接收请求参数和决定视图走向,真正的业务计算放在Service层,数据访问全部封装在DAO层,这条链路就是最经典的MVC落地方式。把分层拆清楚后,核心好处有两个:第一,登录校验、字符集这些横切逻辑不用在每个控制器里重复写;第二,业务规则独立出来之后,后边扩展“预约寄件”“批量导入”等功能只要增改Service方法,不用动前端页面。

2.1 请求链路:一个快递入库请求的前半生

先看一份实际的web.xml配置,它定义了Filter、Servlet和Session过期时间。项目跑在Tomcat 8.5,JDK 1.8,MySQL 8.0,全部用JavaEE原生规范实现。

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.express.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>LoginFilter</filter-name> <filter-class>com.express.filter.LoginFilter</filter-class> <init-param> <param-name>anonUrls</param-name> <param-value>/login.jsp,/static/</param-value> </init-param> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>ExpressServlet</servlet-name> <servlet-class>com.express.controller.ExpressServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ExpressServlet</servlet-name> <url-pattern>/express/*</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>

逻辑说明:EncodingFilter匹配所有路径,统一处理请求体和响应体的字符集,这块不解决后续所有中文乱码都得重来。LoginFilter同样匹配所有路径,但通过init-param里的anonUrls放行登录页和静态资源,其余请求一律检查Session。ExpressServlet使用/express/*前缀映射,这样快递相关的增删改查都收敛到同一个控制器里,doPost内部再根据路径参数分支处理。session-timeout配置为30分钟,用户的登录凭据超过该空闲时间就会失效。

参数说明:anonUrls配置为逗号分隔的路径前缀,例如登录页login.jsp、静态资源目录/static/。需要注意,Filter的拦截范围会影响每个请求,如果匿名路径没有写正确,会出现登录页自己重定向自己的情况。load-on-startup配置成1,表示项目启动时就初始化这个Servlet,避免第一次请求才做装配。

2.2 目录结构:每个包都在解决什么问题

如果只是把JSP乱扔在WebContent根目录,项目也能跑,但维护性很差。我把控制器、业务、数据库操作、实体类、过滤器、工具类全部按包拆开,JSP页面统一放进WEB-INF/jsp目录,这样用户无法通过地址栏直接访问视图层,所有页面跳转都必须经过控制器转发。

src/ ├── main/ │ ├── java/ │ │ ├── com/express/controller/ │ │ │ ├── UserServlet.java │ │ │ ├── ExpressServlet.java │ │ │ └── HistoryServlet.java │ │ ├── com/express/service/ │ │ │ ├── UserService.java │ │ │ ├── ExpressService.java │ │ │ └── impl/ │ │ │ ├── UserServiceImpl.java │ │ │ └── ExpressServiceImpl.java │ │ ├── com/express/dao/ │ │ │ ├── UserDao.java │ │ │ └── ExpressDao.java │ │ ├── com/express/entity/ │ │ │ ├── User.java │ │ │ └── Express.java │ │ ├── com/express/filter/ │ │ │ ├── EncodingFilter.java │ │ │ └── LoginFilter.java │ │ └── com/express/util/ │ │ └── DBUtil.java │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── jsp/ │ │ ├── login.jsp │ │ ├── index.jsp │ │ ├── expressList.jsp │ │ └── history.jsp │ └── static/ │ ├── css/ │ └── js/

说明分层职责:controller层只做三件事——接收参数、调用Service、转发视图;service层负责业务规则,比如快递入库前要查重复单号,取件时要判断包裹当前状态;dao层只关心SQL和结果集映射;entity是数据库表对应的普通POJO;filter层放通用逻辑;util层提供数据库连接和关闭方案。以后出问题时,按照“页面 → 控制器 → Service → DAO → SQL”的顺序排查,能省掉大量盲目debug时间。

2.3 Filter与Session的组合:登录状态怎么保持

登录校验这块,网上很多教程是让每个Servlet都检查一遍Session,这个项目改用Filter统一拦截。用户登录成功后,我会把userId和userName这两个属性放进Session;LoginFilter则对每个请求做一次三元判断:当前路径是否属于匿名路径,或者当前Session里是否存在userId。

// com.express.filter.LoginFilter public class LoginFilter implements Filter { private String[] anonUrls; @Override public void init(FilterConfig filterConfig) { String anon = filterConfig.getInitParameter("anonUrls"); if (anon != null && !anon.isEmpty()) { anonUrls = anon.split(","); } } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); String url = request.getRequestURI(); boolean isAnon = false; if (anonUrls != null) { for (String prefix : anonUrls) { if (url.startsWith(request.getContextPath() + prefix)) { isAnon = true; break; } } } if (isAnon || (session != null && session.getAttribute("userId") != null)) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } } @Override public void destroy() {} }

逻辑说明:这里最值得留意的是request.getSession(false),它只获取已有的会话,如果用户没有登录过,就不创建多余的Session对象,避免每个静态资源都往内存里塞一个无效会话。anonUrls从web.xml的init-param里读取,例子中放了/login.jsp和/static/,实际可以根据页面调整。如果请求既不匿名,Session里又没有用户ID,就重定向到登录页。

参数说明:anonUrls改成任意路径前缀都可以,多个用英文逗号分隔;如果要临时放行某个Servlet,加上对应的url-pattern前缀即可。注意Session的key要和登录Servlet里设置的key完全一致,否则过滤器永远认为用户未登录,就会出现“登录成功后仍然跳回login.jsp”的诡异问题。

3. 核心业务实战:从登录到入库、取件、查记录的完整实现

这一章是项目真正能吃透业务的地方。视觉上看到的JSP只是表象,核心在于三个动作:登录注册时如何把用户身份安全放进Session,快递入库时如何防重复、防脏数据,取件时如何保证包裹状态不被并发改错。业务链路虽短,但每个环节都涉及Controller接收参数、Service支撑业务、DAO与MySQL交互。我按业务流程一个一个拆开讲。

3.1 登录与注册:Session装配与密码处理

登录模块我单独放在UserServlet里,注册和登录都用同一个控制器,通过action参数区分。用户在login.jsp输入手机号和密码,提交到/user/login,UserServlet调用UserService完成校验。下面这段是登录逻辑的简化版。

// com.express.controller.UserServlet @WebServlet("/user/*") public class UserServlet extends HttpServlet { private UserService userService = new UserServiceImpl(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getRequestURI().substring(req.getContextPath().length()); if ("/user/login".equals(action)) { handleLogin(req, resp); } else if ("/user/register".equals(action)) { handleRegister(req, resp); } } private void handleLogin(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String phone = req.getParameter("phone"); String password = req.getParameter("password"); User user = userService.login(phone, password); if (user == null) { req.setAttribute("errorMsg", "手机号或密码错误"); req.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("userId", user.getId()); session.setAttribute("userName", user.getName()); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } private void handleRegister(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user = new User(); user.setName(req.getParameter("name")); user.setPhone(req.getParameter("phone")); user.setPassword(DigestUtil.md5(req.getParameter("password"))); boolean ok = userService.register(user); if (ok) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); } else { req.setAttribute("errorMsg", "该手机号已注册"); req.getRequestDispatcher("/WEB-INF/jsp/register.jsp").forward(req, resp); } } }

逻辑说明:doPost先把URI末尾的路径截取出来,例如/express/user/login取到/user/login,用这个字段做分支判断,避免在一个Servlet里堆满if-else判断请求参数。handleLogin里先调用Service拿到User实体,为空说明账号密码不匹配,直接转发回登录页并携带错误提示;校验成功则把userId、userName塞进Session,然后重定向到首页。密码在注册时用MD5做散列再落库,登录校验时同样对输入密码做散列,再与数据库比对,避免明文存储。

参数说明:action来自URL路径,不是隐藏字段;resp.sendRedirect之后,浏览器会发起第二次请求,所以session里必须提前存入用户信息。这里简化了验证码和防暴力破解,真实上线需要再加拦截器和验证码生成器。

3.2 快递入库:Controller收参,Service防重,DAO落库

入库是快递e栈最核心的动作。业务员在网页上填写快递单号、收件人手机号、柜子编号,提交到/express/save。这段代码展示了从Servlet到Service再到DAO的完整流转。

// com.express.controller.ExpressServlet 节选 @WebServlet("/express/*") public class ExpressServlet extends HttpServlet { private ExpressService expressService = new ExpressServiceImpl(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getRequestURI().substring(req.getContextPath().length()); if ("/express/save".equals(action)) { saveExpress(req, resp); } else if ("/express/pickup".equals(action)) { pickupExpress(req, resp); } } private void saveExpress(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Express express = new Express(); express.setExpressNumber(req.getParameter("expressNumber")); express.setReceiverPhone(req.getParameter("receiverPhone")); express.setShelfCode(req.getParameter("shelfCode")); String msg = expressService.save(express); if ("SUCCESS".equals(msg)) { resp.sendRedirect(req.getContextPath() + "/express/list"); } else { req.setAttribute("errorMsg", msg); req.getRequestDispatcher("/WEB-INF/jsp/expressForm.jsp").forward(req, resp); } } }
// com.express.service.impl.ExpressServiceImpl public String save(Express express) { // 快递单号和手机号不能为空 if (express.getExpressNumber() == null || express.getExpressNumber().isEmpty()) { return "快递单号不能为空"; } if (express.getReceiverPhone() == null || express.getReceiverPhone().isEmpty()) { return "收件人手机号不能为空"; } ExpressDao dao = new ExpressDao(); if (dao.existsByExpressNumber(express.getExpressNumber())) { return "该快递单号已入库"; } express.setStatus(1); // 1:在库 0:已取 express.setInTime(new Timestamp(System.currentTimeMillis())); dao.insert(express); return "SUCCESS"; }

逻辑说明:控制器层把请求参数封装成Express实体,Service层承担业务校验和状态设置,DAO层负责真正的数据库写入。这里有个实用设计——Service返回的是业务提示文案,而不是布尔值,这样前端可以把各种失败原因直接展示给用户,不需要在控制器里再做一遍判断。快递单号重复检查放在插入之前,status初始化为1表示包裹在库,inTime写入当前时间戳。

参数说明:expressNumber和receiverPhone是前端表单的主键字段;shelfCode是柜子编号,用于取件时定位包裹。如果库表对expressNumber建立了唯一索引,那么existsByExpressNumber可以省掉,直接用insert异常来兜底,但业务层保留这个校验能给出更友好的提示。

// com.express.dao.ExpressDao 节选 public void insert(Express express) { String sql = "INSERT INTO express(express_number, receiver_phone, shelf_code, status, in_time, out_time) VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, express.getExpressNumber()); ps.setString(2, express.getReceiverPhone()); ps.setString(3, express.getShelfCode()); ps.setInt(4, express.getStatus()); ps.setTimestamp(5, express.getInTime()); ps.setTimestamp(6, express.getOutTime()); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("快递入库失败", e); } }

逻辑说明:这段DAO代码使用了try-with-resources,省去手动关闭连接的样板代码,代码里所有语句都走PreparedStatement参数绑定,防止SQL注入。每次获取连接都调用DBUtil.getConnection(),DBUtil底层可以通过DriverManager创建连接,也可以后续替换成连接池。这里我把时间字段设置成Timestamp类型,正好对应MySQL的datetime列。

参数说明:executeUpdate返回的是受影响行数,正常插入一行会返回1;如果SQL执行抛出SQLIntegrityConstraintViolationException,多半是expressNumber重复,说明Service层校验还没生效或者并发场景下两个请求同时通过了exists检查。

3.3 取件与记录查询:更新状态与分页列表

取件流程正好与入库相反,操作员输入快递单号,系统先检查状态是不是在库,如果在库则更新status为0,并记录out_time;如果已经是已取状态,则提示“该包裹已被取走”。记录查询则使用简单的分页逻辑,每页显示10条快递记录。

public boolean pickup(String expressNumber) { String querySql = "SELECT id, status FROM express WHERE express_number = ?"; String updateSql = "UPDATE express SET status = 0, out_time = ? " + "WHERE express_number = ? AND status = 1"; try (Connection conn = DBUtil.getConnection()) { // 关闭自动提交,保证查询与更新在同一事务 conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(querySql)) { ps.setString(1, expressNumber); try (ResultSet rs = ps.executeQuery()) { if (!rs.next() || rs.getInt("status") != 1) { conn.rollback(); return false; } } } try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setTimestamp(1, new Timestamp(System.currentTimeMillis())); ps.setString(2, expressNumber); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (SQLException e) { throw new RuntimeException("取件操作失败", e); } }

逻辑说明:这里我用事务控制保证了两步操作的原子性——先查状态再更新状态不是一条SQL,如果中间发生异常,必须回滚,否则会出现“状态查询显示在库,但实际更新失败”的不一致。updateSql里带上AND status = 1,是一个非常关键的细节,它让数据库在更新时再做一次状态校验,即使多个请求同时进来,也只有一个能把status从1改成0。

参数说明:conn.setAutoCommit(false)之后,所有操作都处于未提交状态;执行完update后,rows是实际影响的记录数,如果为0说明当前包裹已经不是“在库”状态,直接回滚返回false。取件成功后,前端可以跳转到历史记录页看到out_time,或者弹窗提示操作成功。

4. 避坑与常见问题排查:部署后反复踩的坑桩桩数给你听

项目不大,但部署和调试时遇到的问题一点也不少。我在这里整理了五个最常遇到的坑,每条都是真实发生过、并且会反复在别人项目里重演的。按“现象、原因、解决”的结构写,方便你直接对照排查。

4.1 中文乱码:明明JSP设置了UTF-8还是乱

现象:浏览器页面上快递单号里的“丰”字变成“???”,数据库里存的也是乱码。

原因:JSP页面头部的contentType只影响响应体编码,请求参数在进入Servlet时如果没有设置request.setCharacterEncoding,POST表单的中文默认按ISO-8859-1解析,乱码已经发生了。MySQL连接URL里如果缺少characterEncoding=UTF-8,写进去的Transit字符到了数据库也会被转换错位。

解决:在web.xml中配置的EncodingFilter是所有过滤器里最靠前的一个,里面执行request.setCharacterEncoding(encoding)和response.setCharacterEncoding(encoding),并且把connector的URIEncoding属性也设为UTF-8。JDBC连接串统一加上?useUnicode=true&characterEncoding=UTF-8,三层同时处理,乱码才能根除。

4.2 Filter循环重定向:登录后跳回登录页,页面不停刷新

现象:用户登录成功后跳转index.jsp,结果又被拦截返回login.jsp,不断循环,浏览器提示重定向次数过多。

原因:LoginFilter的anonUrls里忘记放行login.jsp,导致请求/login.jsp本身也要登录校验。登录页请求拿不到Session里的userId,于是被重定向回login.jsp,形成死循环。

解决:把/login.jsp和/static/这类页面配置进anonUrls参数,确保匿名页面走白名单过滤。排查时先看浏览器Network面板请求了哪些路径,点击任意一条看到302重定向到login.jsp,就能定位到具体拦截位置。

4.3 重复取件:两个人同时来取同一包裹,都显示成功

现象:操作员同时处理两个屏幕上的同一快递单号,两边都执行取件,结果数据库里只更新了一次,但界面上都提示成功。

原因:取件业务查询状态后需要再更新,原本分开写,两个线程同时查到status=1,然后都执行update,后执行的一方会覆盖前一方的时间戳和状态。update语句没有带上条件status=1,导致重复更新。

解决:把update语句改成UPDATE express SET status = 0, out_time = ? WHERE express_number = ? AND status = 1,再检查受影响行数,只有返回1才算取件成功。同时用事务控制查询和更新的顺序,避免中间插入其他会话的修改。

4.4 数据库连接泄漏:跑一上午之后系统突然卡死

现象:系统运行到下午,所有页面都加载很慢,Tomcat报“too many connections”错误。

原因:JDBC的Connection对象创建后没有关闭,代码里的try-with-resources只覆盖了PreparedStatement和ResultSet,Connection仍然挂在数据库服务器上。时间一长,连接池耗尽,新请求无法获得连接。

解决:DBUtil.getConnection()创建连接的地方全部改用try-with-resources,例如try (Connection conn = DBUtil.getConnection())包裹整个业务方法,并保证数据库驱动,连接串、用户密码都与实际环境一致。上线前用JMeter压一遍,观察连接数曲线,可以快速暴露泄漏点。

4.5 页面直接访问WEB-INF下的JSP导致404

现象:浏览器地址栏输入http://localhost:8080/express/WEB-INF/jsp/index.jsp,直接返回404错误,但通过控制器转发又正常。

原因:Servlet规范规定容器默认不对外暴露WEB-INF目录下的内容,直接访问请求被Tomcat拒掉。这是保护机制,不是工程配置错误。

解决:保持JSP放在WEB-INF/jsp下,所有页面跳转都用控制器转发或重定向,连静态资源也统一放进static目录。平常调试想看某个JSP效果,可以通过Servlet路径访问,或者先复制到webapp根目录临时预览。

5. 二次开发指南:从单Servlet拆到前端控制器与Maven构建

想继续在这个项目上叠加功能,第一步不是加新Servlet,而是先把多个Servlet里重复的“路径解析、参数封装、视图跳转”逻辑收敛起来。快递e栈目前是UserServlet、ExpressServlet、HistoryServlet各管一摊,页面一多这种写法会膨胀。常见做法是引入一个BaseServlet基类,内部提供getParam(String)和forward(String)两个方法,子类继承后减少样板代码。更彻底的办法是学习前端控制器模式,用一个DispatcherServlet统一接收所有请求,根据URI映射到不同的Handler方法。

第二个值得动手的改造点是构建方式。如果你已经用惯了Maven,建议把项目里的lib目录删除,引入javax.servlet-api、mysql-connector-java、jstl这几个依赖,让Maven自动管理版本。再进一步,可以把连接管理换成连接池技术,例如HikariCP或Druid,DBUtil.getConnection()内部改为从数据源获取连接,这样并发能力立刻上一个台阶。

最后一个建议是前后端分离方向。把JSP页面里的Java片段改成纯HTML + JavaScript,使用fetch请求Controller接口,Controller统一返回JSON数据,前端再渲染快递列表。这个改动能让快递e栈从传统Web应用演变成面向移动端小程序可复用API的架构,也为后续接实时通知、扫码找件留出接口空间。

我从这个项目里学到的习惯是:每次接手老JavaWeb项目,都强制自己先列出Filter链和连接泄漏点,再动业务代码;遇到一次循环重定向和一次取件重复之后,我再也不敢跳过白名单配置和状态校验,直接把这两条写进了个人开发清单。希望你在这个项目上也能少走几步弯路,把Servlet、Filter、Session的应用场景真正吃透。希望帮到你。

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

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

金融IT系统建设为何必须锚定具体业务场景

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题为"financial-services"&#xff0c;这是一个高度泛化的行业术语&#xff0c;本身不构成具体可操作的项目、技术方案或实践主题&#xff1b;项目正文为空&#xff0c;未提供任何实质性描述、功能…

作者头像 李华
网站建设 2026/10/17 9:01:04

Superpowers开发者认知增强工具链:四件套协同原理与实战部署

1. 项目概述&#xff1a;Superpowers 不是超能力&#xff0c;而是开发者工具链的“认知增强层”你搜“superpowers”时看到的满屏Claude Code、Antigravity、Codex CLI、Cursor&#xff0c;不是漫威新片预告&#xff0c;而是一场静悄悄发生的开发范式迁移——它不改变你写代码的…

作者头像 李华
网站建设 2026/10/17 9:00:46

AI写教材查重老飘红?用“信息重建”六步法打造原创内容

最近一段时间&#xff0c;我几乎每隔几天就要回答一次类似的问题&#xff1a;“用AI写教材&#xff0c;查重到底能不能过&#xff1f;”问的人有做企业培训的&#xff0c;有做兼职讲师的&#xff0c;还有出版机构的内容编辑。大家真正想问的其实是&#xff1a;能不能让AI把教材…

作者头像 李华
网站建设 2026/10/17 8:59:47

superpowers:用SKILL.md为AI编码代理立规矩

我最近一直在折腾Claude Code做日常重构&#xff0c;说实话体验时好时坏。模型经常在"答应你"和"干得漂亮"之间反复横跳&#xff0c;最典型的情况就是&#xff1a;你说"先跑测试再动手"&#xff0c;它嘴上答应得很好&#xff0c;下一条消息就把代…

作者头像 李华
网站建设 2026/10/17 8:58:30

从CNN到Transformer:多模态对齐的演进与实战

前阵子帮一个朋友调一个图像检索和描述匹配的小功能&#xff0c;图像模型单独跑、文本模型单独跑都没问题&#xff0c;一旦把两者接到一起&#xff0c;准确率直接跳水。排查到最后发现&#xff0c;问题不在两个模型本身&#xff0c;而在我根本没想清楚它们各自的特征到底该怎么…

作者头像 李华
网站建设 2026/10/17 8:57:45

Android接入Kimi流式API:SSE解析与TextView实时渲染实战

简介&#xff1a;本资源是一套面向Android开发者的KIMI大模型API流式响应集成实践工程&#xff0c;适用于需要在移动端实现低延迟、渐进式AI内容输出的中高级开发者。项目完整呈现了从网络请求封装、SSE&#xff08;Server-Sent Events&#xff09;解析、UI线程安全更新到异常重…

作者头像 李华