没有哪个仓库管理者愿意在月底盘点时发现账实不符几十件也没有哪个仓管员喜欢抱着Excel表格在货架间来回跑。这个项目一开始的诉求很朴素让仓库里的入库、出库、盘点、找货全部在一台Android手机上完成同时保留一个管理后台做基础数据和报表。标题里虽然提到PHP、asp.net、Java、SpringBoot、SSM、Vue3这么一串技术名词其实真正落地时我把技术选型收敛成了SpringBoot SSM Vue3 Android原生这套生态理由后面会细讲。整套系统做下来覆盖了移动端手持操作、后端业务接口、管理后台数据维护三个端。仓库管理员用Android APP扫码入库、扫码出库、盘点核对办公室里的管理员用Vue3后台维护商品档案、供应商信息、查看库存流水和报表后端统一用SpringBoot整合SpringMVC和MyBatis提供Restful接口。这篇文章不写概念性的东西直接说清楚我是怎么设计的数据库怎么建接口怎么写Android端哪里容易踩坑Vue3后台又该怎么接。1. 项目背景与需求拆解这套系统到底解决了什么问题1.1 仓库管理场景里的核心痛点先聊一个真实的场景。一家做五金配件分销的小型仓库库位大概两千个SKU数量四千多每天出库单量在一百五十单左右。以前的管理方式是什么入库时仓管员对照送货单用笔在纸质单据上打钩然后自己找个空位把货放好再把“某货放在某位”记在自己的脑子里。出库时按订单拣货找不到货就满仓库转。月底盘点就更要命全员停下手里的活拿着纸质盘点表一货一货数数完回去录Excel再跟系统账面数对差异一次盘点折腾两天。这套仓库管理APP瞄准的就是这些具体矛盾。扫码替代手工录入库存变动实时同步出入库记录全部留痕盘点时直接扫一个核对一个。把“人找货”变成“系统找货”把“事后对账”变成“实时记账”。1.2 功能边界与角色梳理整个系统涉及三类角色。超级管理员负责初始化仓库、库位、商品分类和用户账号仓管员是Android端最核心的使用者执行扫码入库、扫码出库、移库、盘点业务管理员通过Vue3后台查看流水、导出报表、调整库存。权限上采用RBAC模型后台通过用户角色控制菜单可见性Android端则通过接口权限控制操作入口。功能模块上没有贪多核心就是六大块登录认证、库存看板、入库管理、出库管理、盘点管理、基础数据维护。库存看板要能看到每个SKU的总库存、可用库存、所在库位入库分为采购入库和退货入库出库对应销售出库和领料出库盘点采用盲盘和明盘两种模式。其余像批次管理、保质期管理、多仓库调拨这些属于二期迭代内容第一版不做避免项目失控。1.3 前后端分离与多端协同的总体思路多端协同指的是Android手持端和Vue3管理后台共享同一套后端接口。Android端安装在仓管员手机上或工业PDA上Vue3后台跑在办公室电脑的浏览器里。两端虽然UI形态差别很大但调用的是同一批RESTful API读的是同一套数据库这就避免了“账在Excel里、系统里又有一套数”的老毛病。项目结构上我没有用传统的单体JSP项目而是前后端完全分离。后端只负责业务逻辑和JSON输出Android和Vue3各自渲染。这样做的好处是两端可以并行开发后端定好接口契约后Android端开发人员和管理后台开发人员各自开工互不阻塞。联调阶段用Swagger管理接口文档前端直接在线调试。2. 技术选型分析为什么最终落在SpringBootSSMVue3Android这套组合上2.1 PHP和asp.net为什么被排除标题里特意带上了PHP和asp.net说明这个项目在早期调研阶段确实考虑过这两条路线。PHP的优势是部署成本低Laravel框架开发速度快做小型信息管理系统很合适但考虑到仓库业务后续要处理库存扣减这类强事务操作以及未来可能接入电子秤、AGV调度等硬件接口PHP的生态和工程化程度明显不如Java。更何况团队在Java方向上的积累更深维护成本更低没必要为了“快”牺牲长期可维护性。asp.net的性能和工具链其实不错IdentityServer做认证、EF Core做ORM都很成熟但在国内中小团队里懂C#的成员不好招部署在Linux服务器上也绕不开.NET运行时。Android端调用asp.net接口本身没毛病但整体生态和招聘资源明显向Java倾斜。做技术选型不是选一个最强的而是选一个团队能长期驾驭的这一点在仓储系统这种需要持续迭代的B端项目里尤其关键。2.2 SpringBoot与SSM的互补关系很多刚入门的朋友会把SpringBoot和SSM理解成两个对立的东西其实它们是叠加关系。SSM是Spring SpringMVC MyBatis的组合Spring管Bean依赖SpringMVC管HTTP请求分发MyBatis管SQL和结果映射。而SpringBoot是建立在Spring之上的自动化配置框架解决了大量XML配置的繁琐问题。这套项目里的做法是用SpringBoot作为项目骨架starter直接引入依赖自动装配数据源和事务管理器Web层依然用SpringMVC的注解方式写Controller数据访问层依然用MyBatis写SQL和Mapper接口。所以本质上我是“在SpringBoot里跑了一套SSM架构”。这样既保留了SpringMVCMyBatis那种清晰的分层和SQL可控性又享受了SpringBoot的自动配置和独立启动能力发布时一个Jar包直接部署。2.3 Android原生与Vue3后台的选型逻辑Android端我选的是Java原生开发没有用Flutter也没有用UniApp。原因在于仓库APP要频繁调用原生能力尤其是扫码摄像头调用、蓝牙打印机连接、系统级广播监听这些硬件交互原生的兼容性调试路径最短。虽说UniApp也能调原生插件但出问题时排查链条太长仓管员在现场等着用系统不稳定几回就会被打入冷宫。管理后台用Vue3 Vite Element Plus这套组合看中的是组件化开发效率。仓储管理后台的表单多、表格多Element Plus的表格组件支持自定义列、排序、筛选省掉大量无意义的样板代码。Vue3的组合式API处理复杂页面的逻辑复用也比Vue2的Options API舒服得多特别是库存看板这种既要实时刷新又要多条件筛选的页面。3. 系统总体架构与数据库设计表结构决定业务边界3.1 前后端分离与部署架构整个系统在物理上分成三个节点Android客户端或PDA、Vue3管理后台Nginx部署、SpringBoot服务端独立Jar运行。服务端连接MySQL数据库Redis做登录令牌缓存和热点库存数据的缓存。这里多说一句Redis的用处库存看板首页需要汇总多张表的数据每次都查MySQL压力大我把汇总结果以JSON形式缓存到Redis设置5分钟过期看板接口的响应时间从平均230ms降到了50ms以内。接口通信统一采用JSON格式认证采用JWTJSON Web Token。用户登录成功后服务端签发一个有效期24小时的Token之后所有请求都在Header的Authorization字段带上这个Token。Redis里额外存一份当前登录用户的权限标识集合用于接口级别的权限判断。3.2 核心数据表设计详解数据库是整套系统的地基。很多新手在设计表时容易犯的错误是只想着存当前数据不考虑业务追溯。我在这个项目里遵循一个原则凡是能留痕的操作必建单据表凡是单据必然带明细子表。用户表t_user保存用户基本信息、角色ID、状态供应商表t_supplier保存供应商名称、联系人、电话商品表t_goods保存SKU主数据包括商品编码、名称、规格、单位、默认库位、状态仓库表t_warehouse和库位表t_location记录物理位置库存表t_stock记录每个商品在每个库位上的数量。单据侧设计了入库单和出库单两张主表入库单t_stock_in记录单号、供应商ID、经手人、入库类型、入库时间、审核状态入库明细表t_stock_in_item记录商品编码、数量、库位、批次号。出库单t_stock_out结构对称包含出库类型和收货方信息。以下是一份简化后的商品表和库存表建表SQL放在这里方便理解字段设计CREATE TABLE t_goods ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_code varchar(32) NOT NULL COMMENT 商品编码, goods_name varchar(128) NOT NULL COMMENT 商品名称, specification varchar(64) DEFAULT NULL COMMENT 规格型号, unit varchar(16) DEFAULT NULL COMMENT 计量单位, default_location_id bigint(20) DEFAULT NULL COMMENT 默认库位ID, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1启用 0停用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_goods_code (goods_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品主数据表; CREATE TABLE t_stock ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_id bigint(20) NOT NULL COMMENT 商品ID, location_id bigint(20) NOT NULL COMMENT 库位ID, quantity decimal(12,2) NOT NULL DEFAULT 0 COMMENT 当前库存数量, locked_quantity decimal(12,2) NOT NULL DEFAULT 0 COMMENT 锁定数量预留, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_goods_location (goods_id, location_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存表;3.3 库存表设计的特殊考量库存表的唯一索引我设计成goods_id, location_id意思是同一个商品在同一个库位只允许一行数据。这个约束在批量导入和并发写入时非常关键如果业务上允许多行数据存在那库存汇总和扣减都会变得极其痛苦。数量字段用了decimal(12,2)而不是int因为很多仓库会管理辅助单位比如按箱入库再拆包按个出库箱和个之间的换算会产生小数。整张表没有soft delete字段库存记录只增改不做物理删除这是为了保证出入库流水能对上账。当某个库位的库存扣减到零时我选择保留记录把quantity更新成0而不是把记录删掉这样历史流水中的库位线索不会断裂。4. 后端核心接口与SSM整合实战从配置到业务闭环4.1 工程结构与关键依赖后端工程是标准的Maven多模块拆分但实际上我没有强行分多个Maven模块因为项目体量没到那个程度拆多了反而增加启动复杂度。我采用的是单模块下的分包策略controller、service、mapper、entity、common、config。common里放统一返回体、异常处理、JWT工具类、全局校验。pom.xml里的依赖集中在spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、jjwt、hutool-all、druid-spring-boot-starter、spring-boot-starter-data-redis、knife4j这几个。Druid负责数据库连接池和监控knife4j生成接口文档Hutool帮我们处理字符串和日期转换这些都是经过实践验证的省心组合。dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency4.2 统一返回结果与全局异常处理前端无论是Android还是Vue3都需要一套稳定的接口返回格式。我统一包装成Result对象code为0表示成功非0表示业务失败data放业务数据message放错误提示。这样做的好处是前端完全不用关心HTTP Status是200还是500只需要判断code。即使后端抛了空指针全局异常处理器也会兜底捕获返回code500的JSON而不是让前端拿到一堆看不懂的堆栈信息。public class ResultT { private Integer code; private String message; private T data; // getter/setter 省略 }全局异常处理用RestControllerAdvice注解实现。我单独定义了一个BizException业务代码中凡是遇到“库存不足”、“商品不存在”、“单据已审核”这类可预期错误都主动抛出BizException由全局处理器统一转成Result返回。这样做比每个Service方法里手动return一个错误Result干净得多也方便后续在AOP层统一埋点记录操作日志。4.3 基于JWT的登录认证与权限控制登录接口收到用户名密码后先走一次数据库校验校验通过后用jjwt生成Token把用户ID和角色信息放进Claims里。密码存储用的是BCrypt加密绝对不允许明文入库。Token拦截器在WebMvcConfigurer中注册拦截除login和doc路径之外的所有接口。拦截器里解析Header的Token解析成功后把当前用户信息放入ThreadLocal供Controller和Service使用。String token Jwts.builder() .setSubject(userId.toString()) .claim(roleCode, roleCode) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这里给一个实操提醒Token过期时间不要设置太长24小时是合理值因为仓库管理员可能随时换人交接。但也不要太短否则仓管员在库房现场操作到一半Token过期重新登录很麻烦。我在Android端做了一个静默续期的处理客户端在请求返回401时自动用refreshToken换新Token用户全程无感知。4.4 入库和出库接口的完整业务实现入库接口是核心业务逻辑的重点。一个完整的入库操作包含三步创建入库主单、循环插入入库明细、累加库存。整个流程必须在一个事务里任何一步失败都要全部回滚。这里特别强调事务因为新手很容易在循环里单独调用一个update库存的方法结果明细插入成功但库存更新失败账就平不了。Transactional(rollbackFor Exception.class) public Long createStockIn(StockInDTO dto) { // 1. 生成入库单号并保存主表 StockIn stockIn new StockIn(); stockIn.setBillNo(RK System.currentTimeMillis()); stockIn.setSupplierId(dto.getSupplierId()); stockIn.setOperatorId(LoginUserHolder.getUserId()); stockIn.setStatus(0); // 待审核 stockInMapper.insert(stockIn); // 2. 遍历明细插入子表同时操作库存表 for (StockInItemDTO item : dto.getItems()) { StockInItem detail new StockInItem(); detail.setBillId(stockIn.getId()); detail.setGoodsId(item.getGoodsId()); detail.setQuantity(item.getQuantity()); detail.setLocationId(item.getLocationId()); stockInItemMapper.insert(detail); // 尝试插入库存记录若存在则累加 stockMapper.increaseStock(item.getGoodsId(), item.getLocationId(), item.getQuantity()); } return stockIn.getId(); }出库接口的逻辑是对称的但多了一个校验检查当前库存是否足够。库存扣减的SQL必须写成原子操作用一条update语句完成“数量足够才扣减”的判断这是防止超卖的关键。UPDATE t_stock SET quantity quantity - #{quantity} WHERE goods_id #{goodsId} AND location_id #{locationId} AND quantity #{quantity}这条SQL执行后返回受影响行数如果返回0说明库存不足或者记录不存在Service层再抛出BizException提示“库存不足当前可用库存X”。用这种方式替代先查询再判断再更新避免了并发场景下两个请求同时读到相同库存数、都认为可以扣减的问题。5. Android端仓库管理APP的实现要点扫码、持久化、离线兜底5.1 Android项目的分层设计与网络层封装Android端的包结构我采用按业务模块分包而不是按技术类型分包。比如com.example.warehouse包下有login、dashboard、stockin、stockout、stocktake、mine这六个业务包每个包里再包含对应的Activity、Adapter、ViewModel。这样写的好处是需求变更时能精准定位代码而不是在几十个Activity堆里找一个按钮逻辑。网络层使用Retrofit OkHttp组合。Retrofit的接口定义很直观和服务端的Controller一一对应。我把BaseUrl放在BuildConfig里区分debug和release环境调试时指向电脑的局域网IP正式发布时指向服务器域名。OkHttp的Interceptor统一加Token、统一打印日志、统一处理错误码。Android端我用了LiveData做数据驱动更新页面通过观察ViewModel里的状态字段来刷新UI避免手动在Activity里到处写回调。public interface StockApi { POST(api/stock/out) CallResultLong createStockOut(Body StockOutRequest request); }5.2 扫码模块ZXing的接入与踩坑经历扫码是仓库APP最常用的功能。我用的是ZXing库在Android端做了深度定制。ZXing自带的CaptureActivity处理的是全屏扫码仓库里用的工业PDA通常有实体扫码键所以我改造成CameraScan回调模式在Activity生命周期中启动相机预览通过Delegate代理把扫码结果回调出来。PDA的实体扫码键通过KeyEvent监听按下时调用ZXing的decode方法。踩坑最大的一点是ZXing默认的取景框在强光下根本看不清扫描线。仓库现场常有阳光直射我把ScanView改成画四个角的边框加中间一条极细的红色瞄准线背景半透明黑色稍微压暗。另外对条码类型做过滤只允许解析CODE_128、EAN_13、QR_CODE这三种避免货架上的杂码被误扫。扫码得到的结果统一回传到一个商品解析逻辑根据条码内容去后端查询商品信息。这里注意条码不一定等于商品编码有些供应商的条码是69开头的国标码有些企业内部码是自己打印的。我的做法是后端提供多级匹配先按商品编码查查不到再按店内码查再查不到就返回“未识别的条码”并让仓管员手动输入。5.3 入库、出库、盘点三个页面最需要注意的交互细节入库页面的操作流是扫商品条码 - 自动带出商品名、规格、单位 - 输入数量 - 选择库位 - 提交。为了让仓管员不用逐个打字库位我做了最近使用列表默认带出上一次入库时用的库位同时支持扫码枪读取库位条码。这里不要做成弹窗选择器现场戴着手套点小按钮非常痛苦要设置大按钮和行高。出库页面和入库类似但多了“缺货提示”。我在服务端接口里做了可用库存校验Android端提交前也会用本地缓存商品库存先做一次预校验减轻服务端压力。虽然本地预校验不保证绝对准确但能拦截八成明显错误仓管员反馈更好。盘点页面采用的是盲盘模式。盲盘的意思是盘点时不显示系统账面数量仓管员只管扫一个数一个系统实时累加盘完提交后再由服务端对比账面数量出差异。这样避免仓管员受到账面数心理暗示盘点结果更真实。这个模式下界面必须有一个大字号的数量显示区和“本次已盘N件”的进度提示因为扫到一半被叫去处理异常情况是仓库里的常态断了再回来得知道盘到哪了。5.4 登录态、本地缓存与弱网兜底仓库地下库和偏远库房经常信号不好全程依赖实时网络肯定是不能用。我做了两级兜底第一步是登录Token持久化到SharedPreferencesAPP启动时自动恢复登录态第二步是出入库关键操作支持本地暂存。假若网络请求失败Android端会把本次出入库数据写入本地SQLite队列标记为待同步等网络恢复后由WorkManager后台任务批量同步到服务端。本地同步机制看着简单但有一个业务隐患同步顺序不能乱。如果先出库后入库同步时也必须先出后入否则库存校验会误判。我在SQLite里给每条待同步记录加了一个自增序号同步时按升序逐条处理处理成功的删除记录失败的保留并记录错误原因。这个机制在第一版上线后救了不少次尤其是手机掉在货堆里半小时没信号的情况。6. Vue3管理后台的实现细节从脚手架到业务落地6.1 工程搭建与路由权限控制Vue3后台用Vite作为构建工具比Webpack在开发环境下的热更新速度快了一个级别。UI组件库选用Element Plus状态管理用Pinia。工程初始化时我直接选择了Vue Router的createWebHistory模式然后在路由配置里加入全局前置守卫每次路由跳转前检查Pinia里是否有Token没有Token就跳转登录页有Token但所在路由的meta需要指定角色时再比对角色权限。router.beforeEach((to, _from, next) { const store useUserStore(); if (to.meta.requiresAuth !store.token) { next({ path: /login }); } else if (to.meta.roles !to.meta.roles.includes(store.roleCode)) { next({ path: /403 }); } else { next(); } });权限这块我采用了按钮级权限和路由级权限的双重控制。菜单显示用路由守卫控制页面内的新增、删除、审核按钮用v-permission自定义指令控制。服务端接口再加一层校验即使前端绕过按钮直接调接口后端也会拒绝无权限操作。前端控制的是体验后端控制的是安全任何一端都不能遗漏。6.2 库存看板与出入库流水页面库存看板页是后台使用频率最高的页面。左侧放仓库和库位树形选择右侧是库存表格表格列包含商品编码、商品名称、规格、单位、库位、当前库存、锁定库存、更新时间。分页用后端分页每页20条避免一次性加载几千行数据导致浏览器假死。出入库流水页的做法是只读列表默认按创建时间倒序顶部提供时间范围、单据类型、操作人三个筛选条件。这个页面我特意加了导出功能用前端把当前筛选条件下的数据请求全部拉下来通过xlsx插件生成Excel文件。仓库月度对账就靠这个导出上线后操作人员反馈说省了他们过去从系统里一页页复制粘贴的功夫。6.3 Axios封装与接口联调经验Axios我会在创建实例时设置baseURL、超时时间和请求拦截器。请求拦截器统一从Pinia拿Token加到Authorization头响应拦截器统一判断HTTP状态码和业务codecode为401时清除登录信息并跳转登录页。这里最关键的一点是不要把网络错误和业务错误混为一谈我的处理方式是HTTP层错误弹“网络异常请稍后重试”业务层错误使用ElMessage直接展示后端返回的message。联调阶段我把knife4j的接口文档地址发给Android端和Vue3端共用后端用Swagger注解把每个字段的含义标清楚前端对照文档自测。接入Vue3后台时我踩过的一个坑是跨域配置后端在CorsConfig里开放了所有来源这在开发环境没问题但上线前我把它改成了白名单模式。一定不要贪图省事用全放开生产环境跨域配置加个白名单是底线。7. 常见问题排查与避坑实录7.1 中文乱码问题的三个排查位置第一个位置是MySQL连接串必须加上useUnicodetruecharacterEncodingutf8第二个位置是SpringBoot的server.servlet.encoding配置我直接显示指定了UTF-8第三个位置是HTTP响应头确保Content-Type里带charsetutf-8。这三处只要有一处没设置对入库商品名称就会出现乱码。Android端如果发现服务端返回的中文变成问号90%是连接串问题先检查这个地方不要急着在客户端做转码转码治标不治本还容易搞混数据。7.2 Android真机连不上本地后端接口这个问题每个做Android联调的人都遇到过。手机和电脑连同一个WiFi然后在后端启动参数里把address绑定到0.0.0.0否则默认绑定127.0.0.1只能本机访问。之后再查看电脑局域网IP用ipconfigWindows或ifconfigMac/Linux查Android端BuildConfig里的BaseUrl改成http://192.168.x.x:8080。这里还有个隐藏坑Android 9以上默认禁止HTTP明文流量必须在AndroidManifest.xml里给application加上android:usesCleartextTraffictrue否则请求会被拦截报ERR_CLEARTEXT_NOT_PERMITTED。7.3 MyBatis查询出现N1问题库存列表页如果直接暴力循环查询库位名称一条商品记录查询一次库位表页面就会极慢。我在Mapper里用resultMap的association关联查询或者直接写JOIN SQL把关联字段一次查出来。MyBatis的懒加载虽然配置方便但我在实际项目里基本不开启宁愿SQL里全查出来映射也不依赖懒加载来省那点查询量因为懒加载不好控制触发时机容易在序列化和跨层调用时搞出懒加载异常。7.4 ID生成策略与分布式环境考量这个项目单机部署时我用MySQL自增主键没问题但仓库系统后期大概率要接多仓多个仓的数据合并时自增ID一定会撞车。所以我在设计阶段就把全局唯一ID方案定了下来用的是MyBatis-Plus的雪花ID策略。虽然这套项目没引入MyBatis-Plus但我在主键类型上直接采用bigint后续如果切换到MyBatis-Plus或者需要分布式部署不需要改表结构。7.5 事务不生效的经典场景Transactional注解要想生效前提是方法被Spring代理调用也就是说必须通过注入的Service去调用而不能在同一个类里this调用也不能自己new一个Service调用。我见过有人把库存扣减方法写在Controller里然后在Controller里直接this调用另一个带事务注解的方法事务完全没生效出了问题数据却已经写进去了。另外事务方法必须被public修饰private方法上的Transactional是无效的。7.6 扫码识别慢的现场调优仓库里的国产PDA扫码识别速度受光线影响很大。遇到扫码慢先看摄像头是否支持连续自动对焦在相机参数的RepeatedRequest中添加CONTROL_AF_MODE_CONTINUOUS_PICTURE。如果还慢检查ZXing的decode线程是否被UI线程阻塞。我的经验是将decode放到独立线程回传结果通过Handler切换到主线程UI操作绝不占用解码线程。另外对低端PDA要把照片分辨率的预览尺寸降低例如1280x720比默认1920x1080识别速度快不少。8. 折腾完这套系统后我沉淀下来的几点体会如果重新做一遍这个项目我会把条形码标签打印设计提前到第一版。现在很多第三方系统或者Excel管理的仓库货架和商品上根本没有统一的条码你光给人家装一个扫码APP没用必须先帮人家把货位码和商品码全部打印贴好扫码才能跑起来。这件事属于“不上线不知道一上线全卡死”的环节。标签打印用普通的条码打印机就够了格式上建议货位码用CODE_128规格尺寸按40mm×30mm贴在货架横梁上最醒目。一个小技巧分享给你在做账实差异分析时不要只看总额差异要按库位维度拆。我见过盘点差异集中发生在两个相邻库位之间原因就是仓管员放货时把A位和B位搞混了。系统里加一个“差异按库位分组”的小功能排障效率会高很多。这套系统后续扩展的方向我认为有两个一个是把流程审批加进来比如入库单提交后需要主管在手机上审批而不是在后台审批仓库现场等主管回办公室才能验收入库太耽误事另一个是接入电子秤Android端通过串口或蓝牙读取电子秤重量数据自动换算数量并填入入库单彻底消灭手工点数的大宗商品入库场景。这两个方向做完仓库的作业效率还能再上一截。