开头先说说这个系统是干嘛的看到这个标题里的springboot_uniapp汽车销售库存管理系统做过相关开发的朋友应该一秒就能反应过来这是一套典型的前后端分离项目微信小程序做销售端Spring Boot提供后端接口uniapp负责一套代码多端编译。汽车销售行业和普通电商有个很大的区别——商品单价高、SKU属性复杂品牌、车系、车型、配置、颜色、内饰、库存流转链路长从厂家采购入库、门店调拨、展厅展示、试驾车辆管理到最终销售出库每一环都不能错。一套能真正落地的汽车销售库存管理系统核心不是把CRUD做出来而是要把“库存账”管明白把“销售-库存-采购”这条链路的数据打通。这篇博文就把这个项目从架构设计到核心实现完整拆开聊。内容适合正在做毕业设计、准备面试项目或者刚进公司需要快速上手同类业务系统的朋友。我会把技术选型的理由、数据库表结构设计思路、核心接口的实现逻辑、小程序端的关键写法还有我实际开发中踩过的坑全部整理出来。1. 项目整体设计与技术选型思路1.1 为什么选uniapp而不是原生微信小程序先说前端。这个项目选了uniapp而不是直接用微信原生小程序开发这是我在类似项目里比较推荐的做法。原因有三点。第一跨端能力。汽车销售公司往往不只是需要微信小程序老板可能还想要一个H5版本方便在微信公众号里打开或者做个安卓App给销售顾问在平板上用。uniapp是Vue语法一套代码可以同时编译到微信小程序、H5、App虽然不能说完全零成本适配但至少不用三套团队各写一套。第二生态和组件。uniapp的插件市场里有大量现成的组件比如轮播图、商品卡片、下拉刷新、扫码等等直接拿来用比自己写方便太多。尤其是汽车展示需要图片轮播、视频播放这类场景用uni-ui或者市场里的组件能省不少事。第三开发体验。uniapp基于Vue的写法对前端开发者非常友好我见过很多团队用原生小程序开发时被setData的性能问题和复杂数据绑定折腾得不轻而uniapp的响应式数据绑定更贴近Vue的开发习惯。1.2 后端为什么锁定Spring Boot后端选Spring Boot这个基本没什么悬念。汽车销售库存管理系统虽然业务不算极其复杂但涉及用户登录、订单管理、库存台账、权限控制Spring Boot的生态能把这些统统包住。加上它的自动配置机制开发者只需要关注业务逻辑本身不需要像传统Spring项目那样手动配置一大堆XML文件。在这个项目里Spring Boot的核心作用可以拆成几块RESTful API接口层给微信小程序提供数据接口统一返回JSON格式。Spring Security或拦截器 JWT处理登录鉴权区分管理员和销售员角色。MyBatis Plus操作MySQL数据库重点是分页查询和条件构造器开发效率极高。全局异常处理用RestControllerAdvice统一捕获业务异常避免异常信息直接暴露给前端。我之前做过一个对比同样一套管理系统的后端用Spring Boot加MyBatis Plus比用原生Spring加Hibernate开发速度能快大约30%尤其在做条件查询和分页的时候省下的代码量非常可观。1.3 项目的整体模块规划整个项目在功能上拆成两大端四小块。管理端微信小程序内车辆管理模块车辆的新增、编辑、上下架、条件搜索。库存管理模块入库、出库、调拨、盘点、库存预警。销售管理模块创建销售订单、生成出库单、查看销售记录。数据看板模块今日销售、今日入库、库存总量、低库存提醒。后台服务端系统管理管理员账号、员工账号、角色权限。数据接口上述所有业务模块的增删改查接口。报表统计按日/周/月统计销售额和库存变动。2. 数据库设计汽车库存的核心是台账清晰2.1 车辆信息表和库存表的拆分逻辑数据库设计是整个系统的地基我之前见过很多新手项目直接把车辆信息和库存混在一张表里结果后面做盘点、调拨的时候痛苦万分。汽车的“商品属性”和“库存状态”其实是两回事。一辆车有固定的属性比如品牌、型号、排量、颜色、车架号VIN但它的库存状态是动态的——可能在总仓、可能调到了某个门店、可能已经卖出、可能在展厅当试驾车。所以这个项目我把数据拆成车辆信息表car_info和库存表inventory。车辆信息表保存静态属性CREATE TABLE car_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, vin VARCHAR(32) NOT NULL COMMENT 车架号, brand VARCHAR(64) COMMENT 品牌, series VARCHAR(64) COMMENT 车系, model VARCHAR(128) COMMENT 车型, model_year VARCHAR(16) COMMENT 年款, color VARCHAR(32) COMMENT 外观颜色, interior_color VARCHAR(32) COMMENT 内饰颜色, guide_price DECIMAL(12,2) COMMENT 指导价, purchase_price DECIMAL(12,2) COMMENT 采购价, sale_price DECIMAL(12,2) COMMENT 销售价, car_status CHAR(2) COMMENT 01在库 02已售 03在途 04试驾, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_vin (vin) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;库存表则记录每辆车的实时位置和仓库信息CREATE TABLE inventory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, car_id BIGINT NOT NULL COMMENT 车辆ID, warehouse_id BIGINT COMMENT 仓库ID, quantity INT DEFAULT 1 COMMENT 数量, inventory_status CHAR(2) COMMENT 01可售 02锁定 03调拨中, create_time DATETIME, update_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么要拆因为同一辆车可能从“在库可售”变成“销售锁定”再到“已售出库”这些状态变更如果都去改车辆表字段会导致大量冗余更新而且不好追溯操作记录。拆开之后车辆属性只维护一份库存状态的变更单独记录逻辑清晰很多。2.2 订单表和出入库单据表的设计要点汽车销售真正做订单和电商下单有本质区别。电商订单是商品维度的一单可以多件商品汽车销售订单基本是车维度的一单对应一台车最多加装潢、保险、金融服务这些附加项。所以这里的**销售订单表sale_order**不搞订单明细子表直接在主表上关联车辆IDCREATE TABLE sale_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) UNIQUE COMMENT 订单号, car_id BIGINT COMMENT 车辆ID, customer_name VARCHAR(64) COMMENT 客户姓名, customer_phone VARCHAR(20) COMMENT 客户电话, sale_price DECIMAL(12,2) COMMENT 成交价, order_status CHAR(2) COMMENT 01待付款 02已付款 03已交车 04已取消, sales_user_id BIGINT COMMENT 销售顾问ID, create_time DATETIME, update_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;库存变动的每一笔单据单独记录用**出入库单据表stock_record**来跟踪CREATE TABLE stock_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, record_no VARCHAR(32) COMMENT 单据号, car_id BIGINT COMMENT 车辆ID, record_type CHAR(2) COMMENT 01采购入库 02销售出库 03调拨出 04调拨入 05盘盈 06盘亏, warehouse_id BIGINT COMMENT 仓库ID, quantity INT COMMENT 变动数量正数增加负数减少, operator_id BIGINT COMMENT 操作人ID, remark VARCHAR(255) COMMENT 备注, create_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表是整个库存管理系统的操作日志也是账实一致的核心保障。每一台车从进入到卖出所有流转记录都能在stock_record里查得到这就是所谓的“可追溯台账”。2.3 用户角色与权限的简洁设计汽车销售库存系统的用户角色不需要像OA系统那么复杂三种角色基本够用管理员拥有全部权限包括员工管理、数据报表查看。销售顾问可以看库存、创建销售订单、操作销售出库但不能做采购入库和盘库。仓库管理员负责采购入库、调拨、盘点不能看销售毛利等敏感数据。我用一张用户表加role字段实现不做独立的关系表。小程序端根据role字段动态隐藏功能入口。这个设计在大多数中小型汽车销售企业里已经够用没必要杀鸡用牛刀去搞Spring Security的RBAC全套。3. 后端核心功能实现从接口到业务逻辑3.1 JWT登录鉴权与拦截器配置微信小程序前后端分离状态管理不适合用传统的Session。原因很简单小程序端和后端接口不在同一个域下SessionId的传递和保存在跨域场景下非常麻烦。这个项目我用JWTJSON Web Token做无状态登录。用户登录流程小程序端把用户名密码通过HTTPS POST到/api/login。后端校验账号密码通过后生成一个JWT Token里面带上用户ID和角色。后端把Token返回给小程序小程序存在storage里。后续请求在header里带Authorization: Bearer token。Spring Boot的拦截器统一解析Token校验通过才放行。拦截器的核心代码如下public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录已过期请重新登录\}); return false; } } response.setStatus(401); return false; } }拦截器配置里要注意登录接口、静态资源必须放行。如果是开发阶段想快速调试建议把swagger相关路径也放行否则调试接口时一堆鉴权干扰。3.2 库存查询接口这就是一个典型的动态条件拼装库存查询是微信小程序端用得最多的接口。销售顾问查库存条件往往不确定今天看品牌明天看车系后天只看颜色。所以接口必须支持多条件组合查询。我用MyBatis Plus的LambdaQueryWrapper来实现动态条件拼装public PageResultCarInventoryVO queryInventory(InventoryQueryDTO dto) { PageCarInventoryVO page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperCarInventoryVO wrapper Wrappers.lambdaQuery(); if (StringUtils.hasText(dto.getBrand())) { wrapper.eq(CarInventoryVO::getBrand, dto.getBrand()); } if (StringUtils.hasText(dto.getSeries())) { wrapper.like(CarInventoryVO::getSeries, dto.getSeries()); } if (StringUtils.hasText(dto.getColor())) { wrapper.eq(CarInventoryVO::getColor, dto.getColor()); } if (StringUtils.isNotBlank(dto.getStatus())) { wrapper.eq(CarInventoryVO::getCarStatus, dto.getStatus()); } wrapper.orderByDesc(CarInventoryVO::getCreateTime); return PageResult.of(baseMapper.selectPage(page, wrapper)); }这里有个细节车系查询用的是like而不是eq因为同一个车系下可能有很多具体型号比如“宝马3系”下面有325Li、320Li用like匹配“3系”能查出整个系列体验更好。3.3 销售出库的库存扣减与事务处理销售出库是这个系统业务上最核心、最需要小心的环节。简单来说就是创建订单 - 锁定车辆 - 客户付款 - 车辆出库。这里必须保证库存状态和订单状态保持一致不能订单一创建车辆就显示“已售”因为客户可能反悔不付款。我的实现思路销售顾问在小程序端选择一辆“在库可售”的车辆创建订单订单状态为“待付款”。后端在创建订单时把车辆的car_status从“01在库”改为“02已售”实际上不够准确更好的方案是先改成“03锁定”同时往stock_record里插入一条“销售锁定”记录。客户完成付款销售顾问点击“确认交车”后端再把订单状态变成“已交车”车辆状态改成“02已售”同时生成正式的销售出库记录。如果订单取消车辆状态从“锁定”恢复为“在库”。这一步我用Transactional保证原子性因为涉及订单表、车辆表、库存记录表三个表的写入任何一个失败都必须回滚Transactional(rollbackFor Exception.class) public void createSaleOrder(SaleOrderCreateDTO dto) { // 1. 校验车辆存在且在库 CarInfo car carInfoMapper.selectById(dto.getCarId()); if (car null || !01.equals(car.getCarStatus())) { throw new BusinessException(车辆不存在或不可售); } // 2. 生成订单号 String orderNo generateOrderNo(); // 3. 插入订单 SaleOrder order new SaleOrder(); order.setOrderNo(orderNo); order.setCarId(dto.getCarId()); order.setCustomerName(dto.getCustomerName()); order.setCustomerPhone(dto.getCustomerPhone()); order.setSalePrice(car.getSalePrice()); order.setOrderStatus(01); order.setSalesUserId(dto.getSalesUserId()); saleOrderMapper.insert(order); // 4. 锁车 car.setCarStatus(03); carInfoMapper.updateById(car); // 5. 写库存变动记录 stockRecordMapper.insert(buildRecord(orderNo, dto.getCarId(), 03, -1, 销售锁定)); }这条链路上库存锁定和订单创建是强一致的关系放在同一个事务里没毛病。但是“付款”和“交车确认”尽量不要放在事务里做外部交互因为等待支付结果会导致数据库长事务这一点在我后面的问题排查章节还会细说。3.4 采购入库和多门店调拨汽车4S店经常有这种情况同一集团下面多个门店A门店有台白色轩逸卖不掉B门店正好有客户要那就得做调拨。我的实现是在stock_record里记录两条记录调拨出库-1和调拨入库1同时把车辆所属仓库改成目标仓库。这里需要后台一次性写好两个仓库的库存变动逻辑并且用事务包裹。采购入库相对简单车辆首次录入系统时直接添加到car_info同时往inventory表插入一条库存记录往stock_record插入一条入库记录。这里有一个业务习惯需要注意采购入库的采购价属于敏感数据接口返回给小程序时不能把这个字段带出去我在VO对象里直接用JsonIgnore注解剔除避免销售员看到采购价影响谈价策略。4. 微信小程序端核心功能开发与坑4.1 管理后台首页的“数据看板”怎么写小程序端首页我做了数据看板四个核心卡片今日销售台数、今日入库台数、当前库存总数、低库存预警数量。这个接口后端可以分别写多个接口但我建议为了减少请求次数直接在后台聚合一个dashboard接口返回。public DashboardVO getDashboardData() { DashboardVO vo new DashboardVO(); vo.setTodaySaleCount(saleOrderMapper.countTodaySale()); vo.setTodayInStockCount(stockRecordMapper.countTodayInStock()); vo.setTotalInventory(carInfoMapper.countByStatus(01)); vo.setLowStockCount(carInfoMapper.countLowStock(threshold)); return vo; }小程序端用uni.request封装请求封装的时候要注意请求拦截器里统一带上token。用uni.addInterceptor或者封装一个公共的request方法都行。我习惯封装一个request.js在成功回调里判断res.data.code如果是401就跳转登录页。4.2 车辆列表页的下拉刷新和触底分页加载车辆列表是销售顾问每天都要看的页面这里有两个交互必须做好下拉刷新和触底加载更多。uniapp的页面生命周期有个内置方法onPullDownRefresh但注意这个方法在小程序端要生效必须在pages.json里对应页面配置enablePullDownRefresh: true否则写上代码也不执行。触底加载用onReachBottom生命周期配合分页参数onReachBottom() { if (this.pageNum * this.pageSize this.total) { uni.showToast({ title: 没有更多了, icon: none }); return; } this.pageNum; this.loadList(); }这里我要重点提醒一个极易踩的坑列表图片的懒加载和缓存。汽车图片往往比较大如果不用懒加载列表会卡死。uniapp的image标签默认支持懒加载但需要在标签上显式写lazy-loadtrue。另外图片域名必须在小程序后台配置到“downloadFile合法域名”或者“业务域名”里否则真机上直接加载不出来开发工具里却一切正常。4.3 条件筛选和扫码识车的实现微信小程序端在车辆列表页我加了筛选栏顶部是品牌下拉、车系输入、颜色选择。uniapp原生的picker组件在微信小程序里运行稳定但它默认只能单选做级联筛选体验一般。我建议用uniapp的popup弹窗自定义筛选表单这样既能做成多条件同时筛选又不影响列表页操作流畅度。扫码功能也是汽车库存系统里一个很实用的点。仓库管理员每天要盘点库存如果一辆一辆翻列表核对效率太低。这个项目里我用uniapp的uni.scanCode接口直接扫描车辆合格证上的二维码或条形码就能定位到对应车辆uni.scanCode({ scanType: [barCode, qrCode], success: (res) { this.getCarDetail(res.result); } });res.result就是扫码得到的结果我在后端把车架号VIN转成了二维码贴在车上扫码直接通过VIN查车非常方便。VIN是车辆的“身份证号”唯一且不可变非常适合作为扫码的识别依据。4.4 uniapp在微信小程序端的打包注意点uniapp项目开发完需要发行到微信小程序端运行。用HBuilderX打包时步骤看起来很简单选中项目点“发行-小程序-微信”然后在微信开发者工具里打开dist目录。但有几个配置细节不处理好打包后一定出问题。第一manifest.json里的微信小程序配置必须填写正确的AppID。测试的时候可以用测试号但上线必须用自己的小程序AppID。很多新手在开发者工具里能跑但真机上打开白屏或者接口请求全部失败基本都是AppID和合法域名的问题。第二ES6转ES5的选项建议开启。微信小程序的JavaScript引擎对ES6的支持度不如浏览器完整有些语法在开发者工具里没问题但在老版本微信客户端里会报错。HBuilderX打包时勾选“ES6转ES5”能避免大量兼容性问题。第三分包加载。如果系统功能多、页面多首包体积很容易超过微信的2MB限制。我的做法是把销售模块、库存模块作为主包把数据分析、系统设置这些低频访问的页面放到分包里。uniapp对分包的支持比较成熟在pages.json里配置subPackages即可{ subPackages: [ { root: pages/report, pages: [ pages/report/dashboard, pages/report/saleTrend ] } ] }4.5 小程序端与后端的接口联调联调阶段有一个很典型的问题本机调试时小程序真机访问不到电脑上的Spring Boot服务。原因很简单手机和电脑不在同一个局域网时访问不到电脑的localhost。解决方案是手机和电脑连同一WiFi后端启动时让Spring Boot监听0.0.0.0小程序里的baseUrl改成电脑的局域网IP比如http://192.168.31.45:8080/api。但要注意微信小程序在正式环境是不允许请求http://的只允许https://而且域名必须在小程序后台备案并配置到白名单。开发阶段在开发者工具里可以勾选“不校验合法域名”但真机预览时必须关掉这个选项所以联调阶段尽量把后端临时配上HTTPS证书或者用内网穿透工具把本地服务映射成一个HTTPS域名。5. 系统安全与性能优化经验5.1 SQL注入和XSS防护汽车销售系统里车辆搜索的keyword参数是最容易被注入的地方。MyBatis Plus的LambdaQueryWrapper本身对参数做了预编译处理只要不使用${}拼接SQL基本可以避免SQL注入。但有个隐患是排序字段如果允许前端传排序字段名容易被构造恶意字段名。我的做法是后端写死一个排序字段白名单前端传过来的字段名不在白名单内就默认按创建时间排序。XSS方面重点是用户输入内容中的script标签。比如客户地址、订单备注这些字段虽然系统没做成富文本但不排除有人在输入框里粘贴恶意代码。我在后端加了一个全局过滤器对请求体中的特殊字符做转义处理public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } }XssHttpServletRequestWrapper继承HttpServletRequestWrapper重写getParameter和getInputStream方法把、等字符转义成lt;、gt;。这个过滤器可以说是通用安全配置里的常客。5.2 库存并发处理的兜底策略库存扣减最怕的就是“超卖”。汽车这种高单价低频商品超卖一台就是几十万的损失。我在创建订单时除了用事务保证一致性还必须加一个乐观锁或者数据库层面的条件更新兜底。update car_info set car_status 03 where id ? and car_status 01这条SQL务必检查受影响行数。如果返回影响行数为0说明车辆被别人先锁定了直接返回“车辆已售”提示int rows carInfoMapper.lockCar(dto.getCarId()); if (rows 0) { throw new BusinessException(手慢了车辆已售或已锁定); }注意事务的隔离级别MySQL默认的可重复读REPEATABLE READ在这个场景下是够用的不需要调到串行化否则性能损耗太大。5.3 数据量大了之后这个项目哪里会先扛不住如果这个系统真的用在一家大型经销商集团日订单量几百单几年下来数据量肯定不小。最先扛不住的会是stock_record表因为每台车从入库到出库至少产生2-3条流水几千台车就是上万条记录。我的建议是stock_record表按月分表或者定期归档。列表页面不要用count(*)实时统计全表用缓存或者离线汇总。车系、品牌这些维度做索引避免全表扫描。不过实话实说对大多数中小型汽车销售企业单表几百万数据配合合理索引MySQL性能是完全够用的没必要过早引入Redis和分布式架构。技术选型要匹配业务规模这句话真的值得反复说。6. 开发排障实录这些坑我帮你踩过了6.1 微信开发者工具报“url not in domain list”开发阶段最经典的问题。小程序访问的接口域名没有在小程序后台配置合法域名。解决分两种情况开发调试微信开发者工具的“详情-本地设置”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。正式上线登录微信公众平台在“开发管理-开发设置-服务器域名”里配置request合法域名。注意这个域名必须备案而且必须HTTPS。6.2 Spring Boot接口返回时间格式不对微信小程序端显示时间经常出现“2025-03-01T10:00:00.00008:00”这种格式前端拿到后还要自己解析。这是Spring Boot默认的Jackson序列化配置导致的。解决办法是在application.yml里统一配置时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样后端返回的时间就直接是2025-03-01 10:00:00小程序端拿过来直接展示省去一层转换。6.3 uniapp的onLoad和onShow重复请求问题小程序页面从列表页跳详情页再返回有些开发者发现数据被重复加载。原因在于onLoad只在页面创建时执行一次但onShow每次页面显示都会执行。我写得久了养成的习惯是首次请求放onLoad后续刷新放onPullDownRefresh这样逻辑职责清晰而且不会有额外的重复请求。如果需要从详情页返回后刷新列表数据可以用uni.$emit和uni.$on做事件通信比在onShow里盲请求强得多。6.4 事务方法内部调用不生效这是Spring事务一个非常经典的坑。在同一类中一个方法调用另一个带Transactional的方法事务是不会生效的。因为Spring的事务代理是通过AOP实现的内部调用绕过了代理对象。我一开始写代码时把createSaleOrder和内部的updateCarStatus写在同一个Service类里结果库存扣减失败时订单却创建成功了。排查了半天最终把内部调用改成注入自身Service或者拆到另一个类事务才真正生效。6.5 数据库连接池连接泄漏系统运行一段时间后接口突然变慢然后报Connection is not available, request timed out。这是典型的数据库连接池泄漏。排查逻辑很直接看是不是有查询忘了关闭连接。如果用MyBatis Plus大多数情况框架会帮你管理连接但如果你在代码里手动写了SqlSession或者Connection务必在finally里关闭。最好的方式是用try-with-resources语法比如try (SqlSession session sqlSessionFactory.openSession()) { // 业务逻辑 }我之前就在一个导出Excel的功能里踩了这个坑每次导出都手动开了连接没有关闭跑了一周导出功能突然全挂重启才好。排查这类问题建议在配置文件里临时把HikariCP的maximum-pool-size调小让连接池更快被耗光问题复现的速度会更快。写到最后我还是得说点实际的。汽车销售库存管理系统这类项目技术上没有太多炫酷的东西就是Spring Boot加uniapp加MySQL三件套很多功能模块在网上一搜一大把。但真正值钱的不是代码本身而是对业务逻辑的理解库存状态怎么流转、订单和车辆怎么关联、出入库台账怎么做到可追溯、并发情况下怎么防止一车卖两次。如果只是照着代码生成器做一个CRUD系统面试官问一句“你这些表为什么这么设计”基本上就露馅了。把这个项目的表结构、事务边界、状态机流转逻辑吃透比多刷一百道面试题都有用。最后分享一个小经验这类系统做完之后强烈建议自己手动跑一遍完整的业务链路——采购入库一台车把它调到分店再创建一个销售订单卖出然后到库里刷新看库存有没有对。我每次开发完都会这样全流程模拟测试一遍因为很多问题只有真实跑完整条链路才会暴露出来。尤其是跨表的业务单纯看代码很难发现逻辑漏洞动手跑一遍数据一比问题就藏不住了。