从购物车到地址簿:后端用户模块落地与ThreadLocal拦截器实战

发布时间:2026/10/12 14:42:10

从购物车到地址簿:后端用户模块落地与ThreadLocal拦截器实战 DAY 6这个以“苍穹外卖”为蓝本的后端项目终于从管理端切到了用户端真实交易链路的前置环节。前面那几天如果按常规节奏走环境搭建、员工登录、分类管理、菜品管理、套餐管理这些后台功能基本已经收尾Spring Boot MyBatis Redis 这套骨架也已经稳定跑起来了。今天要做的事情非常具体把用户端的购物车和地址簿两个模块完整落地。你别看这两个模块名字朴素它们恰恰是后面“提交订单”这个核心动作的数据地基也是很多人在做类似外卖项目时最容易写得马虎、最后被各种并发和越权问题反噬的地方。这篇文章按我第六天的真实开发顺序把数据表怎么设计、接口怎么写、用户身份怎么传递、哪几个坑必须避开全部完整还原出来给正在做这类项目的朋友一份能直接对照落地的方案。1. DAY6 的整体思路为什么先做购物车和地址簿1.1 今天要完成的需求清单打开这一天的需求列表内容其实很集中主要就两块。用户端购物车向购物车添加菜品或套餐、查询购物车列表、减少购物车中某个商品的数量、清空购物车。用户端地址簿新增收货地址、查询当前用户的地址列表、修改地址、删除地址、设置默认地址。如果你拿到这个清单就直接开始写接口很容易犯一个我早期经常犯的毛病上来就开 Controller写到哪算哪结果 Service 层里面全是重复代码查当前用户 ID 的逻辑散落在各个方法里后面想统一加权限都找不到地方下手。所以我第六天真正动手之前先花了一些时间做三件事把接口清单完整列出来、把两张核心表的结构定下来、把“当前登录用户身份从哪来”这个全局问题先解决掉。这三件事做完后面每个接口都变得很机械基本就是照着定好的套路填业务进去。1.2 为什么这两个模块要先于下单实现外卖业务的核心节奏是用户选菜、加入购物车、选择收货地址、提交订单、商家接单、配送。下单接口在做的时候需要一次性接收“购物车商品列表、收货地址 ID、备注、期望送达时间”这些数据。如果购物车和地址簿没有独立的查询和写入接口下单接口就会变得无比臃肿要么自己维护临时购物车状态要么把一整串地址数据直接塞进 JSON 里提交这在工程上是典型的坏味道。把购物车和地址簿先做扎实后面订单模块的职责会非常清楚查询购物车列表生成订单明细根据地址 ID 补全收货人信息下单成功后清空购物车。每个模块各干各的事接口边界和事务边界都能分得清后面出问题时排查起来也轻松得多。这也是我当时坚持把这两个模块先做完的原因不然后面写订单时一定会回头来补各种查询接口。1.3 用户身份是这一天的隐藏主线单看购物车和地址簿好像只是几张表加几个 CRUD。但这两个模块有一个共同点所有操作都必须限定在当前登录用户身上。也就是说A 用户不能查到 B 用户的购物车A 用户也不能把 B 用户的默认地址改掉。这引出了整个项目贯穿始终的一个问题如何在服务端拿到“当前请求属于哪个用户”。第六天之前用户端的登录流程已经打通了前端会拿着一个 JWT 令牌来访问业务接口。我的做法是用拦截器统一解析 JWT把用户 ID 放进一个基于 ThreadLocal 的上下文对象里这样在 Service 层任何位置都能通过BaseContext.getCurrentId()拿到当前用户 ID不需要每个方法都从 Controller 一层层往下传参数。这个方案不是我想出来的花活而是非常经典的 Web 后端做法。它有个显著好处后面写订单、写支付回调、写用户积分这类功能时只要拦截器覆盖到了任何一层都能随时使用用户身份。第六天提前把它做好后面能省掉大量重复代码。2. 购物车模块表结构、接口逻辑与三个高频隐患2.1 购物车表结构一份字段都不能少很多新手设计购物车表时喜欢精简觉得只要有“用户 ID 菜品 ID 数量”就够了。这个认知在外卖场景下是不完整的。因为购物车列表页要直接展示菜品名称、图片、口味、单价如果这些数据全靠查询时再去关联菜品表那每打开一次购物车都要做一次多表关联性能差不说遇到菜品被下架或者改名时历史购物车数据也会跟着变样。购物车里的数据其实是一种“下单前快照”把用户加入购物车那一刻的商品信息冗余到这里。第六天我落地的表结构大致是这样CREATE TABLE shopping_cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, dish_id BIGINT DEFAULT NULL COMMENT 菜品ID与套餐二选一, setmeal_id BIGINT DEFAULT NULL COMMENT 套餐ID与菜品二选一, dish_name VARCHAR(64) COMMENT 冗余菜品名称, dish_flavor VARCHAR(256) COMMENT 冗余口味描述, setmeal_name VARCHAR(64) COMMENT 冗余套餐名称, image VARCHAR(255) COMMENT 冗余图片, number INT NOT NULL DEFAULT 1 COMMENT 数量, amount DECIMAL(10, 2) NOT NULL COMMENT 单价快照, create_time DATETIME DEFAULT NULL COMMENT 加入时间, UNIQUE KEY uk_user_dish (user_id, dish_id, dish_flavor), UNIQUE KEY uk_user_setmeal (user_id, setmeal_id) ) COMMENT 购物车表;这里有一个非常容易被忽略的细节一道菜如果有不同口味比如“微辣”和“特辣”它们必须作为两条购物车记录分别存在。你如果只用dish_id做唯一判断那么用户加了微辣之后再加特辣后一条要么覆盖前一条要么直接保存失败。所以我把(user_id, dish_id, dish_flavor)做成组合唯一索引查重时也必须把口味条件带上。dish_id和setmeal_id二选一这个约束在数据库层没法很方便地做到所以必须在 Service 层保证添加菜品时setmeal_id置空添加套餐时dish_id置空。一旦出现一个记录两个 ID 都非空或者都为空后面统计金额时就会算错。2.2 添加购物车的接口逻辑与查重条件添加购物车接口我设计成POST /user/shoppingCart/add请求体大致长这样{ dishId: 1, setmealId: null, dishFlavor: 微辣 }Controller 层只做参数接收业务全在 Service 层。核心逻辑可以简化为三步第一步从上下文拿当前用户 ID第二步查购物车表里有没有相同记录第三步有记录就数量加一没有记录就新建一条。我在项目里写的 Service 方法大致如下Override public void addShoppingCart(ShoppingCartDTO dto) { Long userId BaseContext.getCurrentId(); ShoppingCart cart new ShoppingCart(); cart.setUserId(userId); cart.setDishId(dto.getDishId()); cart.setSetmealId(dto.getSetmealId()); cart.setDishFlavor(dto.getDishFlavor()); ShoppingCart existing shoppingCartMapper.selectByUserAndDish(cart); if (existing ! null) { existing.setNumber(existing.getNumber() 1); shoppingCartMapper.updateNumberById(existing); return; } ShoppingCart newCart buildNewCart(dto, userId); shoppingCartMapper.insert(newCart); }这里有两个现场经验必须重点说。第一个经验查重条件必须是“用户 ID 菜品 ID 口味”如果是套餐则是“用户 ID 套餐 ID”。我做这个功能时曾经只在查重 SQL 里带了user_id和dish_id结果用户把同一个菜加了两遍但没有口味区分时就会相互覆盖。后来把口味条件加上去这个问题才消失。第二个经验新增购物车记录时金额和名称一定要回表查数据库里的最新值绝不能信任前端传过来的金额。前端页面展示的价格可能滞后如果用户停留在页面很久再点添加传上来的很可能还是旧价格直接用就会造成结算金额错误。前端数据不可信这是做交易相关模块的基本底线。另外如果菜品是停售状态添加时必须抛业务异常提示“菜品已停售”。这一步很多人会忘了做等后面下单结算时才发现用户购物车里躺着一堆已下架的商品那时候处理起来就痛苦了。2.3 数量增减、清空接口的边界处理查询购物车列表没什么特殊难度按user_id查出来按create_time排序即可。返回给前端的数据里至少要有id、name、image、amount、number、dishFlavor这几个字段前端购物车页面才够用。真正需要用心的地方在“减少商品数量”这个接口上。它的请求参数是商品 ID但返回逻辑不是简单地减一。这里有一个很关键的分支判断如果当前商品数量大于 1那就减一如果当前数量等于 1那减一下其实就等于从购物车里移除这条记录。很多人在实现时会顺手写成number number - 1最后库里出现一条数量为 0 的脏数据前端列表渲染时那个商品显示数量为 0看起来非常难受。清空购物车接口就相对简单直接删除当前用户的所有购物车记录。不过这个接口在购物车页面通常不单独出现更常见的场景是下单成功后在订单生成事务里顺手清空购物车。第六天我在写购物车模块时把这个接口先留好就是为了后面 DAY7 下单时可以直接复用让购物车和订单状态保持一致。购物车模块还有一个容易忽略的问题接口返回时如果某个菜品已经不在菜品表里了是直接把这条脏数据过滤掉还是照常返回我的做法是过滤掉并同步删除这条购物车记录因为用户点进去也没办法提交一个不存在的商品留着只会让前端多写一堆容错逻辑。3. 地址簿模块默认地址事务与接口落地细节3.1 地址簿表结构别只设计成四五个字段收货地址在外卖业务里不是一个简单的“省 市 区 详细地址”字符串它至少要包含收货人姓名、手机号、性别标签、详细地址、标签类型、默认标记。我第六天建的地址簿表字段大致如下CREATE TABLE address_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, consignee VARCHAR(32) NOT NULL COMMENT 收货人姓名, sex VARCHAR(8) COMMENT 性别, phone VARCHAR(16) NOT NULL COMMENT 手机号, province_code VARCHAR(16) COMMENT 省编码, city_code VARCHAR(16) COMMENT 市编码, district_code VARCHAR(16) COMMENT 区编码, address_detail VARCHAR(128) NOT NULL COMMENT 详细地址, label VARCHAR(16) COMMENT 标签家/公司/学校等, is_default TINYINT NOT NULL DEFAULT 0 COMMENT 是否默认地址 0否 1是, create_time DATETIME DEFAULT NULL, update_time DATETIME DEFAULT NULL ) COMMENT 地址簿表;有些项目会直接把省市区存成中文文本字段province、city、district。这样也能用但一旦后面要对接配送、对接运费计算、对接区域营销活动中文文本会出现“北京市”和“北京”这种不一致问题非常难受。所以我更倾向于存行政区域编码展示时再由前端或者一个公共接口去映射成中文。当然你如果确定这个项目后面不会接触这类场景直接用文本也问题不大只是要提前想清楚。地址簿还有一个容易被忽略的业务要求订单生成后如果用户把地址删了历史订单的收货信息不能跟着消失。因此真正写下单逻辑时订单表里会冗余一份完整的收货人快照而不是只存一个地址簿 ID。这个设计在第六天不需要实现但做地址簿的时候心里必须有这根弦地址表的记录是允许物理删除的。3.2 默认地址为什么必须用事务保证默认地址是整个地址簿模块里最容易出问题的点一个用户最多只能有一个默认地址。这里我没有选择用数据库约束去限制“同一用户只能有一条is_default1”因为 MySQL 里要优雅地实现这个约束很麻烦用唯一索引加函数索引对版本有要求用触发器又会增加额外复杂度。最务实的方式是在 Service 层用事务来保证。具体逻辑是当用户把某个地址设置为默认地址时先把该用户所有地址的is_default置为 0再把当前地址的is_default置为 1。两步操作在一个事务里执行中途任何一个失败都会整体回滚不会出现数据库里同一个用户有两个默认地址的情况。Transactional(rollbackFor Exception.class) public void setDefault(Long addressBookId) { Long userId BaseContext.getCurrentId(); addressBookMapper.updateIsDefaultByUserId(userId, 0); addressBookMapper.updateIsDefaultById(addressBookId, 1); }这里有一个细节Transactional默认只在抛出RuntimeException时回滚如果你在事务代码里调用了某些会抛受检异常的方法一定要用rollbackFor Exception.class明确指定回滚条件否则你看到数据没变还以为是事务没生效其实只是回滚策略不对。新增地址时也有一个默认地址的联动逻辑如果当前用户一个地址都还没有那第一个新增的地址应该直接设为默认地址。这个规则说起来自然但很容易忘。忘了的话用户第一次下单时就找不到默认地址前端还得再做一次额外选择体验差一个档次。3.3 地址簿接口的校验与越权防护地址簿的接口清单我规划成这样GET /user/addressBook/list查询当前用户地址列表POST /user/addressBook新增地址PUT /user/addressBook修改地址DELETE /user/addressBook/{id}删除地址PUT /user/addressBook/default设置默认地址接口不多但参数校验必须做细。收货人姓名不能为空手机号要符合格式详细地址不能为空标签允许为空。我用的手机号正则比较宽松因为外卖场景下不排除用户填的是座机或者特殊号码太严格的正则反而会把真实用户挡在外面。修改地址时还有一个细节接收参数应该用 DTO而不是直接把实体类作为请求体。实体类一旦暴露给前端前端就可以自己传userId、isDefault、createTime这些字段进来轻则造成脏数据重则就是一个越权修改漏洞。我在项目里修改地址的 DTO 只包含地址簿 ID、收货人、手机号、省市区编码、详细地址、标签这些可编辑字段其余字段一律不允许前端传入。删除地址时一定要做归属校验先根据地址 ID 查出记录判断它的user_id是否等于当前登录用户 ID。如果不相等直接抛业务异常。这一步很多人会漏掉漏掉之后就会出现 A 用户知道了 B 用户地址 ID 就能随意删掉 B 地址的越权事故。类似的问题在关联业务里特别多第六天我把这个校验固化成习惯后面凡是操作数据表记录的地方都会先问一句这条记录是不是当前用户的。4. 用户态信息通道拦截器 ThreadLocal 的正确打开方式4.1 ThreadLocal 拦截器的运行原理要理解这一节得先明白一个 Java Web 的核心事实一个 HTTP 请求在服务端通常是由一个线程从头执行到尾的从 Controller 到 Service 再到 Mapper全程都在同一个线程里。ThreadLocal 就是一个绑定在当前线程上的“私有储物柜”。你在这个线程里的任何地方存入一个值后续所有代码都能随时取出来而且不同线程之间完全隔离。这就像每个进店顾客都有一个带编号的储物柜A 顾客存进去的东西只有 A 能取B 顾客的存到自己的柜子里互不干扰。但这里隐藏着一个大坑Tomcat 这类 Web 容器为了性能会复用线程。线程处理完请求 A 后不会立刻销毁而是放回线程池等待下一个请求。如果请求结束时没有清理 ThreadLocal下一个请求 B 被分配到同一个线程时就会在储物柜里摸到请求 A 留下的用户 ID。这就是传说中的“串号”表现为用户明明登录了却看到了另一个人的购物车。所以 ThreadLocal 的使用准则是请求开始存进去请求结束必须清掉。这个清理动作一般放在拦截器的afterCompletion方法里确保无论业务代码执行成功还是抛异常都能执行到。4.2 拦截器解析 JWT 并写入用户 ID 的完整做法用户端交易的接口都放在/user/**路径下我第六天写了一个专用的 JWT 拦截器负责两件事校验令牌是否有效有效的就把用户 ID 写入 BaseContext。public class JwtTokenUserInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(token); if (StringUtils.hasText(token)) { Claims claims JwtUtil.parseJWT(token); Long userId Long.valueOf(claims.get(userId).toString()); BaseContext.setCurrentId(userId); return true; } response.setStatus(401); return false; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { BaseContext.removeCurrentId(); } }同时需要一个配置类把拦截器注册到 Spring MVC 的拦截器链里Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtTokenUserInterceptor()) .addPathPatterns(/user/**) .excludePathPatterns(/user/login); } }这里有一点要提醒管理端接口和用户端接口如果都使用同一套 Token 生成逻辑最好还是分别写两个拦截器或者在 JWT 里加一个role字段来区分角色。第六天我分了两个拦截器管理端的拦截器把员工 ID 放到另一个上下文变量里用户端拦截器放用户 ID。虽然 BaseContext 里存的内容都是 Long但语义完全不同混在一起会很危险调 A 接口时取出来的却是 B 角色的 ID出了问题非常难查。4.3 这个环节最容易踩的三个坑第一个坑项目里如果同时有自定义 Filter 和拦截器要清楚执行顺序是 Filter - DispatcherServlet - Interceptor。如果你在 Filter 里也想用 ThreadLocal 存用户信息那得等拦截器把用户 ID 解析出来之后再操作顺序搞反了拿到的就是 null。第二个坑JWT 里的字段名不一致。生成 Token 时如果写的是userId解析时却用id去取取到 null 后转 Long 直接空指针。这类问题不大但特别隐蔽因为业务代码不报错只有执行到需要用户 ID 的地方才会炸。排查时第一件事就是打印解析出来的 Claims把字段名对清楚。第三个坑拦截器addPathPatterns的路径匹配比想象中要严格。我见过有人写的路径是/user结果/user/shoppingCart/list这种多级子路径根本没被拦截到导致所有购物车接口都不需要登录就能访问。Spring 的路径匹配规则里单个路径通常不等于该路径下的所有子路径正确写法应该是/user/**。写完拦截器后我做的第一件事就是用两个请求验证带 token 的请求能通过不带 token 的请求返回 401。这个验证成本很低但能发现 80% 的配置问题。5. 常见问题速查与实测排错心得5.1 典型问题速查表第六天开发过程中我一边写一边把遇到的问题记录下来整理成了一张速查表。如果你在后面几天碰到了类似现象可以直接对照排查。现象可能原因排查思路解决建议添加购物车后出现两条相同记录查重 SQL 没带口味条件查看 SQL 条件和购物车表数据查重条件改为用户ID 菜品ID 口味购物车点击减号后数量变成 0sub 接口没有对数量为 1 的情况做删除断点查看分支数量等于 1 时删除记录所有购物车接口返回未登录拦截器路径配置不对检查 addPathPatterns改成/user/**切换账号后看到上一个用户的数据ThreadLocal 未 remove在 afterCompletion 里打日志补上 BaseContext.removeCurrentId()设置默认地址后旧地址仍是默认没有先批量置 0查库看 is_default 字段事务里先 update 全部置 0 再置 1中文出现乱码数据库连接 URL 没配编码查看 JDBC 连接串加 characterEncodingutf-8删除地址时删掉了别的用户的地址缺少归属校验检查 Service 层是否有 user_id 判断删除前校验地址归属不匹配就抛异常这张表里的问题我在做整个项目的过程中基本都遇到过一次甚至多次。它们本身都不是复杂的技术难题但每一个都真实地影响过用户体验。尤其是 ThreadLocal 串号这类问题一旦出现在线上就是比较严重的数据泄露事故自测阶段一定要重点关照。5.2 用户隔离自测的实操套路做购物车和地址簿这种强用户关联功能时我强烈建议在开发完接口后专门做一轮用户 A 与用户 B 的隔离测试。具体做法不复杂在用户端登录接口拿到两个不同账号的 token用 Postman 或者 curl 分别去调用购物车列表、地址列表。A 账号往购物车添加一个商品后用 B 账号的 token 去查询确认看不到 A 账号的数据再用 A 账号的 token 尝试删除 B 账号的地址 ID确认接口返回业务异常而不是成功。curl -X POST http://localhost:8080/user/shoppingCart/add \ -H Content-Type: application/json \ -H token: 用户A的token \ -d {dishId: 1, dishFlavor: 微辣}我当时做这个测试时还真发现了一个问题某个查询接口的 SQL 里忘了带user_id条件导致 A 用户能看到数据库中所有的购物车记录。这个 bug 如果只看功能是否写完根本发现不了但只要做了隔离测试立刻就露馅了。所以用户隔离验证不是可有可无的步骤而是这种用户端模块的必测项。5.3 当天开发收尾时的个人建议第六天快结束时其实我很想把“提交订单”也一起写掉但最后冷静下来还是忍住了。购物车和地址簿涉及的数据一致性、用户身份隔离、默认地址事务这些点如果不好好验完就急着往下走后面排查起来成本会成倍增加。做项目不需要贪快把每个模块当成一个独立的小产品来验收反而会让整个项目的进度更稳。还有一个小习惯是在 Service 入口第一行加日志把当前用户 ID 打出来。这个习惯帮我节省了大量断点调试时间控制台里能看到current userId: 101这样的输出接口有没有拿到正确的用户身份一目了然。等后面做订单模块时这种日志习惯会更值钱毕竟跟钱相关的接口每一步都得知道是谁在操作、操作的是什么数据。
延伸阅读

更多相关文章

2026/10/12 14:42:10

UE5多人合作游戏网络同步实战:从属性复制到Iris的Coop架构设计

1. 项目缘起与整体设计思路1.1 为什么选择UE5来做多人合作游戏做多人合作游戏这件事,放在几年前我可能会犹豫用哪套引擎。Unity的Netcode方案迭代了好几轮,自研方案又太吃人力,直到UE5的Iris网络架构逐步稳定下来,我才真正觉得这套…

2026/10/12 14:42:10

Acolite大气校正实战:从7z解压到Rrs输出的完整流程

简介:面向遥感数据处理与地表参数反演需求,这份Acolite大气校正工具包提供了可直接运行的Python程序与依赖模块。基于MODTRAN/6S辐射传输模型思路,工具可对Landsat、Sentinel等多光谱影像开展大气散射与吸收校正,输出地表反射率结…

2026/10/12 14:42:10

LSTM异常检测:用PyTorch实现生产级KPI时序监控与告警优化

简介:基于LSTM的异常检测是面向AIOps场景的竞赛项目,适合人工智能、计算机相关专业学生及从业者学习实践。资源包含完整Python源码、数据集与文档说明,源码已测试运行通过,可直接复现或二次开发。压缩包共14个文件,以p…

2026/10/12 15:32:30

浏览器播放RTSP监控流:html5_rtsp_player部署与避坑指南

简介:这是一套面向 Web 前端的 HTML5 RTSP 播放器完整源码包,目标是在浏览器中不依赖插件直接播放 RTSP 视频流,适用于网络监控、视频会议等实时流媒体场景。压缩包共 69 个文件,以 57 个 JavaScript 文件为主,覆盖播放…

2026/10/12 15:32:30

“抄代码”的程序员为何高薪?会抄与乱抄的分界线

入行头几年,我特别看不惯一类人:代码基本靠“抄”,收藏夹里存着一堆现成片段,遇到问题先去搜而不是先想,可工资条上的数字却比我好看不少。后来我自己带了小团队、做了两年技术评审、翻过上百份提交记录,才…

2026/10/12 15:32:30

Excel导入实战:从解析到校验的完整链路与excelimportor 0.0.4应用

简介:Excelimportor 0.0.4 是一款面向前端开发者的开源 Chrome 扩展,用于将 Excel 表格数据快速导入网页,并兼容含 iframe 嵌套结构和 select 下拉控件的复杂页面,省去手写解析与元素匹配代码的繁琐过程。压缩包共 15 个文件&…

2026/10/12 15:32:30

用JavaScript实现为知笔记批量导出Markdown:原理、配置与避坑指南

简介:这是一款专为为知笔记用户打造的Markdown导出插件,采用JavaScript编写,主要解决笔记数据以Markdown格式批量导出的需求。插件通过放置到数据存储目录的Plugins目录并简单配置路径参数即可启用,兼容为知笔记4.5以上版本&#…

2026/10/12 15:32:30

jnetpcap源码编译与零拷贝抓包实战指南

简介:本资源是jnetpcap 1.4.r1425-1版本的完整源码包,面向Java网络编程开发者、网络安全分析学习者及需要深度定制封包捕获能力的技术人员,解决Java环境下调用libpcap底层功能的编译与集成难题。压缩包共1256个文件,含285个Java源…

2026/10/12 15:27:28

双目标动态风险感知路径规划:深度强化学习源码包解析

简介:这是一份基于深度强化学习的双目标动态感知路径规划方法Python源码,核心针对犯罪风险与路径距离两个目标进行动态感知,利用深度强化学习生成最优路线推荐。资源适合人工智能、计算机、自动化、电子信息等方向的在校学生、教师或开发者用…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/12 12:01:26

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/12 0:04:22

绝缘子缺陷检测数据集清洗与工业级训练实战指南

简介:本资源是面向电力AI研发人员、工业视觉工程师及智能巡检系统开发者的绝缘子缺陷检测专用YOLO格式数据集,解决无人机航拍场景下绝缘子破损、污闪、积雪等9类典型缺陷的精准识别与定位难题。数据集共2139张真实巡检图像(含训练/验证/测试集…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑