news 2026/10/19 4:16:41

JSP登录互斥方案:同一账号同一时间只能登录一次的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP登录互斥方案:同一账号同一时间只能登录一次的完整实现

简介:面向JSP开发者的单点登录(SSO)实现示例资源,用于解决“同一账号同一时间只能允许登录一次”的常见安全需求。资源围绕OneLogin与MultiLogin两个演示工程展开,包含完整Java源码、JSP页面、标签库与配置文件,展示基于Session机制和过滤器实现登录互斥的完整过程。压缩包共35个文件,涵盖6个Java类、6个class文件、4个JSP页面、15个TLD标签定义、2个Jar库及XML配置文件,整体仅351KB,目录结构清晰,适合中小型Web项目直接参考。已有1409人学习下载。通过学习可掌握Session创建与校验、过滤器拦截处理、并发控制、Session防劫持等关键实现思路,并能根据业务需求扩展出更安全的OAuth2或JWT认证方案,对提升Web系统登录安全性具有实用价值。

1. 同一时间只能登录一次:一套 JSP 登录互斥方案的核心思路

同一时间只能登录一次,这个需求在 JSP 项目里不是靠数据库字段解决的。我整理过一套登录互斥的 JSP 项目,登录页、首页、用户管理页全是 JSP,需求方反复强调一个账号只能在一处登录,方案最后落在内存里维护一张“账号与 sessionId”的映射表,再配合过滤器做二次校验,才把“顶号”这个动作做得干净。适合谁看:正用 JSP/Servlet 写后台管理系统,又不想为了一个登录互斥把 Spring Security 和 Redis 全引进来的开发者。看完这篇,你可以直接照搬核心类,也能明白这套设计为什么长这样。

整套方案的核心可以用一句话概括:登录的时候,先查这张映射表,发现这个账号已有 sessionId,就把那个 session 作废,再把自己的 sessionId 写进去。后续每个请求都带着 sessionId 来跟表里的最新值比对,对不上就弹回登录页。听起来不复杂,但真落地时会踩出一堆坑,比如并发登录、退出误删、多节点部署失效,这些在后面都会有专门章节。

2. 项目结构与在线会话模型:跑通之前先看懂“顶号”怎么拆

2.1 目录结构:Servlet 管登录、Filter 管校验、Listener 管回收

这套 JSP 项目的包结构按“请求入口—业务服务—会话管理”三层划分,拿到源码后先对着目录认一遍职责,后面改起来才不迷路。

single-login-jsp/ ├── pom.xml └── src/ ├── main/ │ ├── java/com/demo/ │ │ ├── controller/ │ │ │ ├── LoginServlet.java # 登录、顶号触发点 │ │ │ ├── LogoutServlet.java # 正常退出,清理注册表 │ │ │ └── CheckOnlineServlet.java # 前端轮询是否被顶号 │ │ ├── filter/ │ │ │ └── SessionCheckFilter.java # 每个请求的二次校验 │ │ ├── listener/ │ │ │ └── OnlineUserListener.java # 会话销毁时回收注册表 │ │ ├── service/ │ │ │ └── UserService.java # 账号密码校验 │ │ └── util/ │ │ ├── SessionRegistry.java # 账号 -> sessionId 映射 │ │ └── SessionKeeper.java # sessionId -> HttpSession 映射 │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ ├── login.jsp │ ├── index.jsp │ └── static/ └── test/

这里的核心设计是那张内存映射表:SessionRegistry 存“账号最新会话是谁”,SessionKeeper 存“怎么找到那个会话对象让它失效”。两者配合,才能在别人登录同一账号时,精准作废旧会话而不是误伤新会话。filter 和 listener 都是围绕这两张表做增删查。项目中真正需要你改业务逻辑的地方只有 LoginServlet 和 UserService,其余类属于基础设施,基本不用动。

2.2 在线会话注册表:一个 ConcurrentHashMap 撑起整个顶号逻辑

SessionRegistry 的实现非常简单,但它决定了并发登录时的正确性。

public class SessionRegistry { private static final ConcurrentHashMap<String, String> ONLINE = new ConcurrentHashMap<>(); public static void put(String username, String sessionId) { ONLINE.put(username, sessionId); } public static String get(String username) { return ONLINE.get(username); } public static void removeIfMatch(String username, String sessionId) { ONLINE.remove(username, sessionId); } }

注意几个参数的含义:put 方法的第一个参数是登录账号,第二个参数是本次登录产生的会话编号,同一个账号再次登录时,后面的 put 会直接覆盖前面的值,覆盖这个动作本身就是“顶号”。get 方法用来查这个账号当前最新的会话编号。removeIfMatch 带三个参数,前两个是账号和会话编号,第三个作为期望值,只有当前映射值等于期望值时才会删除。

这里最有必要解释的是为什么用 ConcurrentHashMap 而不是 HashMap。登录请求可能同时从多个浏览器进来,如果用普通 HashMap,两个线程同时 put 同一个账号时可能互相覆盖产生脏数据,顶号逻辑瞬间失效。ConcurrentHashMap 的 put 操作是线程安全的,多请求并发登录时能保证最终留下的是后登录那一次的 sessionId。实际项目中如果你不放心,还可以在 put 前后打印日志,方便后面排查被顶号的时间点。

2.3 会话监听器:用户退出和会话超时时,别让注册表里留脏数据

会话不是只靠正常退出才销毁的,用户直接关浏览器、会话超过有效期自动失效,这些场景都会触发 HttpSessionListener 的 sessionDestroyed 方法。如果不在这个回调里清理注册表,表的 value 会残留一堆已经失效的 sessionId,后面做顶号判断时容易拿到旧值。

public class OnlineUserListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent se) { // 会话创建时不需要额外处理 } @Override public void sessionDestroyed(HttpSessionEvent se) { HttpSession session = se.getSession(); String username = (String) session.getAttribute("loginUser"); // 没登录过的会话不处理 if (username == null) { return; } String sessionId = session.getId(); SessionRegistry.removeIfMatch(username, sessionId); SessionKeeper.remove(sessionId); } }

这段代码有一个非常关键的细节:清理注册表时用的是 removeIfMatch 而不是带两个参数的 remove。因为一个账号被顶号时,旧会话失效后会立刻触发 sessionDestroyed 回调,此时新会话已经把自己的 sessionId 写进了注册表。如果用普通 remove,会把新会话的 sessionId 一并删掉,导致新登录的用户下一次请求直接被过滤器判定为无效。removeIfMatch 带上了 sessionId 作为匹配条件,旧会话销毁时只删除自己对应的记录,不碰新会话的数据。在 web.xml 里记得注册这个监听器,漏配的话超时销毁的会话永远不会被清理。

3. 核心链路拆解:登录、顶号、请求拦截、前端提示

3.1 登录接口:先查旧会话,再注册新会话,一气呵成

登录逻辑集中在 LoginServlet 的 doPost 方法里,顶号动作发生在密码校验通过之后、新会话注册之前。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || password == null || username.trim().isEmpty()) { response.sendRedirect(request.getContextPath() + "/login.jsp?error=1"); return; } if (!userService.checkLogin(username, password)) { response.sendRedirect(request.getContextPath() + "/login.jsp?error=2"); return; } // 检查这个账号是否已经在线,如果在线则让旧会话失效 String oldSessionId = SessionRegistry.get(username); if (oldSessionId != null) { SessionKeeper.expire(oldSessionId); } // 建立新会话并注册 HttpSession session = request.getSession(true); session.setAttribute("loginUser", username); SessionRegistry.put(username, session.getId()); SessionKeeper.add(session); response.sendRedirect(request.getContextPath() + "/index.jsp"); }

这段代码先把旧会话顶掉,再建立新会话,顺序不能反。如果先创建新会话再去作废旧会话,两个会话在注册表里会同时存在一小段时间,极端情况下旧会话的某个请求刚好穿过过滤器,会短暂读到旧数据。SessionKeeper.expire 方法内部调用的是 HttpSession.invalidate(),一旦执行,旧会话上的所有属性都会被清空,旧浏览器后续任何请求都拿不到 loginUser,自然就被拦截回登录页。注意 expire 执行时不是删除注册表现在这个账号的记录,只是让旧会话对象失效,真正的清理动作由监听器的 sessionDestroyed 回调完成。

参数方面,username 和 password 都做了空值判断,避免空字符串直接进业务层。error=1 表示参数缺失,error=2 表示账号或密码错误,前端 JSP 页面通过 request.getParameter 读取这个参数来展示提示文案。如果你的项目里登录校验走的是某个用户服务接口,只需要替换 userService.checkLogin 的实现,其他部分不用动。

3.2 请求拦截器:每个请求都把 sessionId 跟注册表比一遍

光在登录时顶掉旧会话还不够,因为被顶号的那个浏览器可能还停留在某个页面上,用户继续点按钮发请求,照样会打到后端。SessionCheckFilter 的作用就是把所有请求拦下来,检查当前 sessionId 是否仍然是这个账号的最新会话。

public class SessionCheckFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 对登录接口、登出接口、静态资源和轮询接口放行 String uri = request.getRequestURI(); String contextPath = request.getContextPath(); if (uri.startsWith(contextPath + "/login") || uri.startsWith(contextPath + "/static") || uri.startsWith(contextPath + "/checkOnline") || uri.equals(contextPath + "/logout")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); String username = session == null ? null : (String) session.getAttribute("loginUser"); if (username == null) { response.sendRedirect(contextPath + "/login.jsp"); return; } String currentSessionId = session.getId(); String latestSessionId = SessionRegistry.get(username); // 会话还在但注册表里已经不是这个 sessionId,说明被顶号 if (latestSessionId == null || !latestSessionId.equals(currentSessionId)) { response.sendRedirect(contextPath + "/login.jsp?kicked=1"); return; } chain.doFilter(req, resp); } }

过滤器判断分两步。第一步看这个会话是否还有登录标记,没有就直接回登录页,这覆盖了会话超时和从未登录的情况。第二步才是顶号专用判断:会话还在、loginUser 也在,但注册表里这个账号绑定的 sessionId 已经变成了另一个值,说明有人用同一账号重新登录了,当前会话已经被覆盖。注意 request.getSession(false) 的写法,传 false 表示不主动创建新会话,如果直接调 request.getSession(),很多本来会被拦截的非法请求反而会拿到一个新建的空会话。在 web.xml 里把这个过滤器映射到 /*,并且把 login.jsp、静态资源、轮询接口、登出接口都放掉,否则登录页和登出请求自己会被拦下,出现诡异死循环。

3.3 前端检测被顶号:轮询接口加弹窗提示

后端拦截只能保证被顶号后的下一次请求被重定向,但用户在页面上发呆不点击,就看不到任何变化。前端加一个定时轮询接口,一旦发现会话被顶,立即弹窗并跳回登录页。

setInterval(function () { fetch(contextPath + '/checkOnline') .then(function (res) { return res.json(); }) .then(function (data) { if (data.online === false) { alert('你的账号已在其他设备登录'); window.location.href = contextPath + '/login.jsp?kicked=1'; } }) .catch(function () { // 网络异常时静默,等下个周期再查 }); }, 30000);

对应的 CheckOnlineServlet 不需要查数据库,直接复用 SessionRegistry 的判断逻辑:当前 session 存在、loginUser 存在、且 sessionId 与注册表一致,就返回 online: true,否则返回 online: false。这里有一个参数值得注意:轮询间隔 30 秒是经验值,太长的话用户被顶号后要等很久才看到提示,太短会给服务器造成不必要的压力。如果你希望体验更即时,可以改成 10 秒,代价是每个在线页面每 10 秒多一个请求,用户量大的时候需要评估一下压力。前端用的是 fetch 而不是 jQuery 的 $.ajax,如果你的 JSP 页面还在用老 jQuery 版本,换成 $.getJSON 效果一样,注意检查浏览器兼容性。

3.4 配置要点:web.xml 里决定成败的三段配置

过滤器、监听器、会话超时都需要在 web.xml 里注册,出问题的项目大概率是这里漏了一段。

<listener> <listener-class>com.demo.listener.OnlineUserListener</listener-class> </listener> <filter> <filter-name>sessionCheckFilter</filter-name> <filter-class>com.demo.filter.SessionCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>sessionCheckFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <session-config> <session-timeout>30</session-timeout> </session-config>

session-timeout 的单位是分钟,30 表示会话 30 分钟无操作后自动失效。对登录互斥这个场景来说,会话超时时间最好跟业务方确认清楚,如果业务期望长时间挂机不掉线,session-timeout 要调大,但注册表里对应的会话也会一直占用内存。filter-mapping 的 url-pattern 用 /* 而不是 /,因为 / 不会拦截到 JSP 页面路径。还有一个细节是过滤器放行的路径要跟实际项目的前缀保持一致,比如静态资源如果放在 static 目录,就按上面代码里的前缀判断;如果你项目的 CSS 和 JS 散落在根目录,就需要在放行条件里逐个加后缀判断,很多页面样式加载不出来的问题都出在这里。

4. 运行与验证:本地部署后完整复现“同一时间只能登录一次”

4.1 部署三步走:从源码到 Tomcat 跑起来

这个 JSP 项目可以直接用 Maven 构建成 war 包,也可以直接把 webapp 目录扔进 Tomcat 的 webapps 里跑。我建议使用 Maven 方式,目录结构更清晰,依赖也不容易丢。

mvn clean package -DskipTests cp target/single-login-jsp.war $TOMCAT_HOME/webapps/ cd $TOMCAT_HOME/bin ./startup.sh

启动后访问 http://localhost:8080/single-login-jsp/login.jsp 就能看到登录页。如果你的 Tomcat 端口不是默认的 8080,改 conf/server.xml 里的端口号即可。这里有个小坑:Tomcat 的 webapps 目录下如果已经有同名 war 解压出来的文件夹,需要先删掉旧目录再启动,否则 Tomcat 不会解压新 war,你改的代码根本没生效,页面还是旧的。

4.2 手工验证:用两个浏览器复现顶号全流程

最直观的验证方式是用两个不同内核的浏览器,比如一个 Chrome 一个 Firefox,因为同内核浏览器共享 cookie 的情况比较多,容易误判。操作步骤很简单:第一个浏览器登录 admin 账号,随便点几个页面确认请求正常;再用第二个浏览器打开登录页,用同一个 admin 账号登录。这时候回到第一个浏览器,点击任意一个菜单,观察页面是否被重定向到 login.jsp 且地址栏带 kicked=1 参数,这就是被顶号的标准现象。

更接近真实场景的做法是:第一个浏览器登录后,故意不刷新页面,等第二个浏览器完成登录后,第一个浏览器按 F5 触发一次请求,你会看到页面直接跳走。如果第一个浏览器还能正常访问业务页面,说明过滤器或注册表的比对逻辑出了问题,优先检查 SessionRegistry 是否被正确覆盖。另外,用浏览器的隐身窗口也能模拟不同会话,因为隐身窗口不携带原窗口的 JSESSIONID cookie,可以当作一个独立的浏览器来用,日常调试时比装两个内核浏览器省事。

4.3 用 curl 模拟会话:看 JSESSIONID 如何被“顶号”

浏览器验证靠肉眼看跳转,curl 验证能看到完整的响应头和 cookie 变化,更容易定位问题。我把联调时常用的命令整理在这里,这条链路可以完整演示顶号过程:

# 第一次登录,保存 cookie 到 a.txt curl -c a.txt -d "username=admin&password=123456" \ http://localhost:8080/single-login-jsp/login # 第二次登录同一账号,保存 cookie 到 b.txt curl -c b.txt -d "username=admin&password=123456" \ http://localhost:8080/single-login-jsp/login # 用第一次登录的 cookie 访问首页 curl -b a.txt -i http://localhost:8080/single-login-jsp/index.jsp # 用第二次登录的 cookie 访问首页 curl -b b.txt -i http://localhost:8080/single-login-jsp/index.jsp

预期结果:第一条和第二条命令执行后,a.txt 和 b.txt 里分别记录了不同的 JSESSIONID;第三条命令返回 302,Location 指向 login.jsp?kicked=1;第四条命令返回 200,正常输出页面内容。如果第三条命令也返回 200,说明顶号逻辑没有触发,检查一遍登录接口里是否真的调用了 SessionRegistry.put。注意 curl 的 -c 参数表示把响应里的 Set-Cookie 写入本地文件,-b 参数表示请求时带上这个文件里的 cookie,两个参数要配对使用,很多人忘了在第二次请求时加 -b,导致请求不带 cookie,被过滤器当成未登录直接拦截,现象看起来像顶号成功了,实际是误判。

4.4 联调时看什么:日志、监听器、过滤器顺序

跑通以后别急着收工,先把排查问题时需要看的观测点摸清楚。第一看 Tomcat 日志里有没有异常堆栈,最常见的是 NullPointerException 出现在 SessionKeeper.expire 里,原因一般是旧会话已经过期销毁,keeper 里对应的记录被监听器清掉了,expire 时取出来的是 null,这种情况需要在 expire 方法里加一个 null 判断。第二看 OnlineUserListener 是否真的注册成功,用一个账号登录后查看 catalina.out 里有没有 sessionCreated 相关日志,没有就检查 web.xml 的 listener 配置。第三看过滤器的执行顺序,如果你的项目里还有编码过滤器、权限过滤器,Order 决定先执行哪个,SessionCheckFilter 最好排在编码过滤器后面,否则 request.getParameter 读到的中文可能乱码。第四看 SessionRegistry 里的值是否符合预期,可以在登录和顶号位置临时加日志打印账号和 sessionId,跑一次双浏览器流程,对照日志确认每次 put、removeIfMatch 时的参数。

5. 避坑与排查:顶号逻辑里四个高频翻车现场

5.1 退出登录把下一个人也踢下线了

这个坑现象非常典型:A 在浏览器里登录,B 在另一个浏览器登录同一账号,B 正常使用,A 被顶号。A 的会话失效触发 sessionDestroyed 回调,结果 B 的业务请求突然全部被重定向回登录页,好像 A 的退出操作把 B 也带下了线。原因在 sessionDestroyed 里调用了 SessionRegistry.remove(username),这个删除操作不看 sessionId,直接把账号整个记录删了,B 的会话虽然还在,但注册表里已经查不到这个账号的 sessionId,过滤器自然判定 B 不合法。解决方式就是前面代码里写的 removeIfMatch,删除时把 sessionId 作为匹配条件,旧会话销毁只删自己的记录。从那以后我每次写监听器清理逻辑,都会先确认删除方法是否带条件,只删匹配自己的那一条。

5.2 静态资源页面加载不出来,样式全丢

页面能打开,但 CSS、JS、图片全 404,后台日志里一堆对 .css 和 .js 的访问被重定向到 login.jsp。原因是 SessionCheckFilter 配置成 /* 之后,静态资源请求也走进了过滤器,这些请求没有会话信息,被直接跳回登录页,浏览器拿到的是登录页的 HTML 而不是样式文件内容。解决方式是在过滤器开头加资源类型判断,把以 .css 、.js 、.png 、.jpg 、.ico 结尾的请求直接放行。如果你用 Maven 打包,静态资源路径带版本号参数,比的时候要取 getRequestURI 而不是 getRequestURL,不然带 ?v=20240101 的链接匹配不上,又要多排查半天。

5.3 登录页 JSP 默认创建了 session,顶号判断错乱

没有在 login.jsp 里关闭会话创建,导致用户第一次打开登录页时 server 就给他创建了一个会话。用户填完账号密码提交登录,LoginServlet 里执行 request.getSession(true) 时,拿到的是刚才那个已经存在的会话,登录成功后把这个会话里写入 loginUser,注册表同步记录 sessionId,看起来一切正常。但用户在多标签页操作时会出问题:第二个标签页打开登录页,拿到了一个新的空白会话,此时第一个标签页的会话还是有效的,两个标签页互相切换时请求带的是各自的 sessionId,过滤器比对注册表发现对不上,其中一个标签页就会被误判为顶号。解决方式是在 login.jsp 顶部加 <%@ page session="false" %>,让登录页不主动创建会话,会话只在登录成功后由 LoginServlet 统一创建。这个坑很隐蔽,因为绝大多数情况单标签页操作根本暴露不出来。

5.4 两台服务器部署后顶号完全失效

前面分析的 SessionRegistry 和 SessionKeeper 都是 JVM 内存数据,一台服务器上注册的值另一台服务器完全看不到。项目上线时做了负载均衡,A 请求被分发到节点一登录,B 请求被分发到节点二登录,两个节点各自维护自己的注册表,互相不知道对方的存在,两个浏览器都能同时正常访问。这种情况在开发环境测不出来,因为本地永远只有一台 Tomcat。解决方式是把注册表从内存换成 Redis,登录时把账号、token、过期时间写进 Redis,过滤器每次请求从 Redis 读取最新 token 比对,不再依赖节点内的 SessionRegistry。如果你暂时没有 Redis,也可以先用消息广播让所有节点同步注册表,但实现复杂度比 Redis 高不少,属于临时方案。这类问题我一般建议直接上 Redis,省掉后续所有跨节点同步的麻烦。

6. 二次开发:把内存注册表换成 Redis,解决多节点踢人问题

6.1 改造方向:从 Session 依赖走向 Token 校验

如果你的系统已经做了负载均衡,第一个要动的就是把 SessionRegistry 里的数据迁到 Redis。登录成功后不再用 HttpSession 存 loginUser,而是生成一个随机 token,写入 Redis,带过期时间。

SET login:user:admin 7f3a9c81e2b04f5d EX 1800

后续请求通过自定义请求头 X-Auth-Token 携带 token,过滤器从 Redis 里根据用户名查出最新 token,跟请求里的 token 比对,不一致就是被顶号。用户退出时执行 DEL 删除这个键。这套改造的前端变化不大,把轮询接口从查 session 改成查 Redis 即可。改造过程中建议先保留原有的 SessionRegistry 和 SessionKeeper 不动,新旧逻辑通过一个开关切换,灰度一段时间确认稳定后,再删掉旧代码。

6.2 验证方式与收尾:两个节点一台 Redis 跑通全流程

改造后的验证方法比单机复杂一点:本地起两个 Tomcat 实例,端口分别设为 8080 和 8081,两个实例连同一个 Redis。先用 Chrome 登录节点一,再用 Firefox 登录节点二,两个浏览器操作同一个账号。预期结果是 Firefox 登录成功后,Chrome 的下一次请求立刻失效,因为 Redis 里的 token 已经被 Firefox 登录时覆盖。再测一遍退出场景:Firefox 退出后,Redis 里该账号的 key 被删除,此时 Chrome 再登录不会受到历史残留数据影响。还有一步建议做的是并发登录测试,用脚本同时向两个节点发起登录请求,确认最终 Redis 里留下的是后完成登录那一次写入的 token。从那以后,我每次接到账号互斥的需求,第一句话先问部署拓扑:单机还是集群?单机用 SessionRegistry 省事,集群直接上 Redis,避免写完才发现内存表没法跨节点,推倒重来。希望帮到你。

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

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

Redis Lua脚本调试完整指南:从环境准备到线上排错

写这篇教程的起因&#xff0c;是我上周帮一个同事排查线上问题&#xff1a;一个用 Lua 脚本写的库存扣减接口&#xff0c;偶尔会出现"库存已经扣了&#xff0c;但业务库没记录"的现象。他打开终端第一句话是&#xff1a;"这个脚本我本地跑是正确的&#xff0c;生…

作者头像 李华
网站建设 2026/10/18 6:19:00

ArcGIS按图斑批量出图:Data Driven Pages+Python

简介&#xff1a;《Arcgis按图斑批量出图&#xff08;按卫片图斑批量出图&#xff09;》是一份面向GIS数据处理、测绘遥感与规划人员的实操型图文文档&#xff0c;重点解决在卫片图斑数量大、需要逐图斑导出JPG的场景下&#xff0c;依靠手动出图效率低下的问题。文档围绕ArcGIS…

作者头像 李华
网站建设 2026/10/18 4:19:18

基于YOLOv8的外墙裂缝检测与中英文双版交互系统实战

做建筑外墙巡检的朋友拿着手机里一片开裂墙面的照片来找我&#xff0c;问我能不能让电脑自动识别裂缝&#xff0c;别再造着望远镜一层楼一层楼地人工扫。我第一反应是这个需求并不新鲜——“目标检测嘛”&#xff0c;可等真正把裂缝样本拉出来才发现&#xff0c;细长、低对比、…

作者头像 李华
网站建设 2026/10/18 8:03:37

激光雷达3D目标检测遮挡难题:归因图原理、复现与工程落地

做激光雷达3D目标检测这几年&#xff0c;我最大的感受是&#xff1a;公开测试集上的高mAP都是“虚的”&#xff0c;遮挡一上来&#xff0c;所有模型都会现原形。不光是行人被前车挡成半截这种视觉意义上的遮挡&#xff0c;更麻烦的是点云这种主动传感器绕不过去的物理限制——激…

作者头像 李华
网站建设 2026/10/18 11:00:07

基于SpringBoot的停车场管理系统设计与实现全攻略

1. 项目概述与选题思考1.1 这项目到底解决什么问题做毕业设计或者接私活时&#xff0c;停车场管理系统是个绕不开的经典选题。我最早接触这类项目&#xff0c;是帮一位学弟梳理毕设思路&#xff0c;当时他拿来的需求描述只有一句话&#xff1a;“做一个能管理车辆进出的系统”。…

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

基于YOLOv8的灾难场景人员检测实战:从数据到中英文双语部署

最近在赶一个项目&#xff0c;把基于 YOLOv8 的灾难场景人员检测系统从中英文双语版一路做到可以演示的程度。老实说&#xff0c;刚接到这个需求的时候&#xff0c;我第一反应是“这不就是套个权重跑一下推理吗”&#xff0c;但真正上手之后才发现&#xff0c;灾难救援场景下的…

作者头像 李华