news 2026/10/27 16:51:12

Springboot+Vue库存管理系统源码解析:从后端分层到并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Springboot+Vue库存管理系统源码解析:从后端分层到并发控制

简介:一套基于Springboot和Vue的库存管理系统完整源码,采用B/S架构和MVC设计模式,面向计算机相关专业学生、毕业设计者及企业技术人员,可作为课程设计或毕业项目的参考蓝本。代码中的核心业务模块均经过运行验证,并配有清晰的中文注释,涵盖商品信息管理、出入库操作、库存实时统计等常见功能,有助于读者理解前后端数据交互和分层开发思路。压缩包共包含431个文件,整体大小约21.16MB,资源内容以Java源码、Vue组件、SVG图标、XML配置和JS脚本为主体,同时附有启动脚本、说明文档及演示视频等辅助文件,目录结构按照后端、前端、资源、文档分模块组织,方便按需检索。系统基于Windows环境开发,开发工具选用常规IDE,配合批处理脚本能在几分钟内完成环境初始化,便于学习者迅速进入调试状态。目前已有95人学习下载,尤其适合具备一定Java与SpringBoot基础、希望独立完成环境搭建、功能修改与二次开发的中级学习者。

1. Springboot加Vue库存管理系统源码:一套能直接二次开发的前后端骨架

仓库规模不大但SKU上百,进出库全靠手工记,月底对账对到怀疑人生,这是很多小团队的真实写照。找人定制一套系统报价动辄上万,自己从零写又得折腾一两个月。这套基于Springboot和Vue的库存管理系统源码,恰好落在这个需求的中间地带。后端用Springboot框架提供接口,前端用Vue做页面,商品档案、入库、出库、库存查询、盘点这些仓库核心业务都是完整闭环,代码里带中文注释,结构清晰,没有堆砌无关功能。不管是毕业设计想快速跑通一个前后端分离项目,还是小团队想拿来做内部工具再用业务打磨,这套源码都能直接作为起跳点。

2. 后端Springboot结构拆解:从Controller到Mapper的中文注释实战

2.1 项目分层与包结构:先看清代码往哪放

拿到源码先别急着启动,花十分钟把后端目录结构过一遍。这是一个标准的Maven工程,包名一般是com.xxx.stock这种,往下拆就是controller、service、mapper、entity、config、common这几个包。controller层只做参数接收和结果返回,service层放业务规则,mapper层对数据库操作,entity放着和表一一对应的实体类,config里是跨域和拦截器配置,common里是统一返回结构和自定义异常。

我一般拿到这类源码会先打开pom.xml,看依赖就能猜出这套系统的技术选型思路。这个项目的核心依赖是spring-boot-starter-web、mybatis-plus、mysql-connector-java,选了MyBatis-Plus说明作者想让单表CRUD少写点代码,把精力放在业务逻辑上。MyBatis-Plus内置了BaseMapper,继承它就直接有了insert、updateById、selectById这些通用方法,库存系统里大部分确实是单表操作,用这个框架能省掉一半重复的XML文件。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.x</version> </dependency>

这段依赖是整个数据层的地基。3.5.x是目前最常见的大版本,把通用SQL方法全部封装在BaseMapper里,你的Mapper接口只要继承它,就自动具备单表增删改查能力。要注意MyBatis-Plus并不排斥手写XML,复杂的多表关联查询依然需要自己写SQL,不要一上来就全靠框架生成,库存盘点、出入库流水汇总这类统计查询还是手写SQL更可控。

2.2 实体类与数据库字段映射:注解写法与命名坑

看懂依赖之后,下一步是看entity包里的实体类。库存系统里最典型的就是商品实体,字段一般包含商品ID、商品编码、商品名称、分类、单位、进货价、销售价、状态。实体类上标注@TableName指明对应哪张表,主键字段标注@TableId,这些注解是MyBatis-Plus规范实体和表映射关系的方式。

这里有一个新手经常翻车的点:数据库字段习惯用下划线命名,比如goods_name,Java属性习惯用驼峰命名,比如goodsName。MyBatis-Plus默认开启驼峰映射,正常情况下能自动转换,但如果你手写SQL查出结果却返回null,先检查application.yml里有没有关闭这个配置,再看SQL查询结果的列别名是不是没改成驼峰。

@Data @TableName("goods") public class Goods { @TableId(type = IdType.AUTO) private Integer id; // 商品编码,业务上唯一 private String goodsCode; // 商品名称 private String goodsName; // 分类,比如"原材料"、"成品" private String category; // 单位,比如"件"、"箱" private String unit; // 进货价,单位元,保留两位小数 private BigDecimal purchasePrice; // 销售价 private BigDecimal salePrice; // 状态:0下架,1上架 private Integer status; }

代码里@TableId(type = IdType.AUTO)表示主键自增,对应数据库的auto_increment字段。goodsCode是业务编码,Excel导入商品时通常用这个字段做去重判断,而不是用自增主键。价格字段用BigDecimal而不是double,这是财务相关代码的常识,double计算金额会出现0.1加0.2等于0.30000000000000004的经典精度问题,库存成本累积计算时这个误差会被放大,最后对账对不上都不知道问题出在哪。

2.3 Service层业务逻辑:入库扣减与库存校验在哪写

这套源码里最值得仔细读的是service层。入库、出库、库存查询、盘点,这些操作真正的业务规则都在service里,controller只是薄薄一层壳。以出库为例,逻辑顺序是先查当前库存够不够,不够直接抛异常,够了再扣减库存并写入出库流水。这两个动作必须处于同一个事务里,否则会出现库存已经扣了但流水没写,或者流水写了但库存没扣的脏数据。

@Service @Transactional(rollbackFor = Exception.class) public class StockOutServiceImpl implements StockOutService { @Override public void stockOut(StockOutDTO dto) { // 1. 查商品当前库存 Stock stock = stockMapper.selectByGoodsId(dto.getGoodsId()); if (stock == null || stock.getQuantity() < dto.getQuantity()) { throw new BusinessException("库存不足,当前库存:" + (stock == null ? 0 : stock.getQuantity())); } // 2. 扣减库存,用version字段做乐观锁条件 int rows = stockMapper.reduceStock(dto.getGoodsId(), dto.getQuantity(), stock.getVersion()); if (rows == 0) { throw new BusinessException("库存已被其他操作修改,请重试"); } // 3. 写一条出库流水 StockOutRecord record = new StockOutRecord(); record.setGoodsId(dto.getGoodsId()); record.setQuantity(dto.getQuantity()); record.setOutType(dto.getOutType()); record.setCreateTime(new Date()); stockOutRecordMapper.insert(record); } }

这段是出库逻辑的标准骨架,三个步骤缺一不可。第一步查库存做前置校验,第二步执行扣减,第三步写流水,整段用@Transactional包住,任何一步抛出异常都整体回滚。注意第二步的reduceStock是自定义SQL,它把“库存够不够”的判断下沉到update语句的where条件里,配合version乐观锁,能同时解决超扣和并发覆盖两个问题。

提示:reduceStock对应的SQL写法是UPDATE stock SET quantity = quantity - #{quantity}, version = version + 1 WHERE goods_id = #{goodsId} AND quantity >= #{quantity} AND version = #{version},返回影响行数,为0说明要么库存不足,要么数据被其他请求改动过,Service层直接抛异常让事务回滚。

3. 前端Vue工程与页面实现:路由、Axios封装与表单交互

3.1 Vue工程结构与路由配置:Layout加子路由的页面骨架

前端是一个标准的Vue CLI工程,目录结构不算复杂。views目录放页面级组件,router放路由配置,api目录统一管理接口请求,utils目录里是封装好的axios实例。库存系统页面数量不多,常见的是登录页、商品管理页、入库页、出库页、库存查询页、盘点页,这种规模用Vue Router加Element UI是最顺手的组合。

打开router配置文件,能看到典型的前后端分离路由组织方式。登录页是独立路由,登录后进入的是一个带侧边栏的Layout组件,所有功能页面都作为Layout的子路由渲染在内容区。这种嵌套关系的好处是菜单只写一份,切换页面时侧边栏和顶部栏不重复加载。

import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) const router = new Router({ routes: [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/Index.vue'), children: [ { path: '', redirect: '/goods' }, { path: 'goods', component: () => import('@/views/Goods.vue') }, { path: 'stock/in', component: () => import('@/views/StockIn.vue') }, { path: 'stock/out', component: () => import('@/views/StockOut.vue') }, { path: 'stock/list', component: () => import('@/views/StockList.vue') } ] } ] })

路由懒加载用() => import()写法,Vue CLI构建时会自动代码分割,首屏不用把全部页面一次性加载完。如果需要做权限控制,不同角色看到不同菜单,这套结构也方便扩展成Vue动态路由,由后端根据角色返回菜单配置,前端用addRoutes动态注册。

3.2 Axios封装与接口对接:请求拦截与响应码处理

前端所有接口请求统一走一个axios实例,这个封装直接决定联调效率。封装要解决两件事:请求发出时自动带上登录凭证,响应返回时统一处理业务状态码。库存系统后端接口一般返回{code: 200, data: ..., msg: "操作成功"}这种结构,code等于200是成功,其他都是业务失败,前端拦截器可以直接弹出后端的提示消息。

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 10000 }) // 请求拦截器:带上登录token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['token'] = token } return config }) // 响应拦截器:统一处理业务码 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.msg || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.msg)) } return res }, error => { Message.error(error.message) return Promise.reject(error) } ) export default service

baseURL读环境变量,开发环境在.env.development里配http://localhost:8080,生产环境在.env.production里配正式后端地址,换环境只改一个文件,这是前后端分离项目的基本素养。token放在localStorage是这类管理系统的常用做法,如果对XSS攻击敏感,可以改成后端签发httpOnly cookie,不过那需要后端配合调整。

3.3 商品入库出库页面:动态表格与表单数据组装

入库页面在外表看是个表单,实际要处理两块数据:入库单的基本信息,包括供应商、入库类型、入库日期;入库的商品明细列表。明细部分是一个动态行表格,每行选择商品、填入数量和单价,前端实时计算单行小计。这个交互用Element UI的el-table配合动态数据源就能实现。

<el-table :data="detailList" border> <el-table-column label="商品" min-width="200"> <template slot-scope="scope"> <el-select v-model="scope.row.goodsId" filterable placeholder="选择商品"> <el-option v-for="g in goodsList" :key="g.id" :label="g.goodsName" :value="g.id"> </el-option> </el-select> </template> </el-table-column> <el-table-column label="数量" width="150"> <template slot-scope="scope"> <el-input-number v-model="scope.row.quantity" :min="1"></el-input-number> </template> </el-table-column> </el-table>

动态明细行里,scope.row是当前行绑定的数据对象,v-model直接绑定到对象的属性上,这样不需要额外维护一套临时状态,提交时把这个detailList整个序列化传给后端就行。el-select加filterable属性支持输入搜索,商品数量多了之后不用一页页翻。

库存查询页则是另一套路子,表格加前端分页组件。分页请求要传pageNum和pageSize,响应里带total总数,前端根据total计算总页数。这里容易踩一个前后端参数名不一致的坑,有的接口用page和limit,有的用pageNum和pageSize,对接前先确认后端Controller接收的参数名,不然第一页能出来,翻页必空。

4. 数据库设计与库存核心流程:库存表、流水表与并发扣减

4.1 数据表设计:商品、库存与流水表的关系

建库建表是让这套源码跑起来的第一步,也是理解整套系统设计思路的关键。库存系统的表结构有一个经典设计:商品表存商品静态信息,库存表单独一张存当前实时库存,入库流水表和出库流水表分别记录每一次库存变动明细。商品表和库存表是一对一关系,通过goods_id字段关联,库存表单独拆出来而不是在商品表里加一个quantity字段,是因为流水表需要记录每一次变化的来龙去脉,账实核对时必须有完整的审计链路。

CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, goods_code VARCHAR(50) NOT NULL UNIQUE, goods_name VARCHAR(100) NOT NULL, category VARCHAR(50), unit VARCHAR(20), purchase_price DECIMAL(10,2), sale_price DECIMAL(10,2), status TINYINT DEFAULT 1 ); CREATE TABLE stock ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL UNIQUE, quantity INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, update_time DATETIME ); CREATE TABLE stock_in_record ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, quantity INT NOT NULL, in_type VARCHAR(20), operator VARCHAR(50), create_time DATETIME );

goods_code的UNIQUE约束是业务上的第一道防线,Excel导入商品重复数据时会被数据库直接拒掉。stock表的version字段是乐观锁标记,每次修改quantity时version加1,防止并发操作互相覆盖。流水表只保留create_time不设update_time,因为流水是不可变记录,一旦允许修改,审计就失去意义了。

4.2 入库出库事务与并发控制:把校验写进SQL

入库操作要干两件事:商品之前没有库存就insert一条库存记录,已有库存就update数量。出库反过来。这里最常见的翻车写法是:先select查库存,代码判断够了再update扣减,两个操作之间存在时间差,两个人同时操作时就可能把库存扣成负数。正确做法是把数量判断直接写进update语句的where条件,由数据库行锁保证同一时间只有一个事务能更新这一行。

UPDATE stock SET quantity = quantity - #{quantity}, version = version + 1 WHERE goods_id = #{goodsId} AND quantity >= #{quantity} AND version = #{version}

这个SQL是并发控制的核心。where里的quantity >= #{quantity}保证库存不会扣成负数,version = #{version}保证是基于最新数据操作,如果影响行数为0,说明条件不满足,Service层抛异常回滚,流水也不会写入。数据库行锁天然串行了对这个商品库存的并发更新,比在应用层加锁要可靠得多。

事务边界也要理清楚。一次完整出库包含扣库存、写流水、可能还要更新商品最后出库时间,这些操作必须在同一个事务里。Springboot项目里在Service实现类上加@Transactional即可,但要指定rollbackFor = Exception.class,因为默认配置只回滚RuntimeException,自定义的BusinessException如果不是继承RuntimeException,事务不会自动回滚,大量新手在这里吃过亏。

4.3 盘点与对账:差异记录怎么设计

盘点功能是库存系统里最能体现设计水平的模块。常见做法是盘点时先展示当前账存数,然后录入实际盘点数,系统自动计算差异生成盘点差异记录,确认后再统一调整库存表。盘点调整绝不能直接改库存表,必须先写差异记录,再走一次调整流水,这样库存变化的链路才是完整的,审计时能说清楚每一笔差异的来源。

-- 盘点差异记录 CREATE TABLE stock_check ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, book_quantity INT NOT NULL, real_quantity INT NOT NULL, diff_quantity INT NOT NULL, check_date DATE, status TINYINT DEFAULT 0, create_time DATETIME );

book_quantity是账存数,real_quantity是实盘数,diff_quantity是差异量,正数盘盈,负数盘亏。status为0表示待确认,1表示已调整。这个设计落地到业务流程里,盘点单可以打印出来给仓库负责人签字确认,确认后再执行调整操作,避免误操作直接污染库存数据。如果做月度对账,直接按check_date聚合diff_quantity,就能快速定位当月哪些商品盘亏最严重,倒推是入库漏记还是出库漏记。

5. 启动部署避坑:从环境到运行的五个常见问题

5.1 后端启动失败:端口占用与数据库连接

现象:IDE里启动Springboot,控制台报Port 8080 was already in use,或者卡了半天报Failed to configure a DataSource。

原因:端口被占用说明本机有别的进程占了8080;DataSource报错九成是application.yml里的数据库地址、账号密码和本机MySQL环境对不上。这套源码里内置的配置可能指向作者自己的数据库地址,不改直接跑必然报错。

解决:先改端口,在application.yml里把server.port换成8081或别的空闲端口,注意前端axios封装的baseURL也要同步改,否则前端调接口还是走8080。数据库连接串重点检查url、username、password三项,MySQL 8.x驱动要配com.mysql.cj.jdbc.Driver,url后面加上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,缺了时区参数直接启动失败,这个报错信息还特别隐晦。

5.2 前端依赖安装报错与Node版本冲突

现象:npm install执行到一半报错,常见的是node-sass编译失败、ERR! code ELIFECYCLE,或者依赖装完后npm run serve能起来但页面白屏,控制台刷一堆语法报错。

原因:node-sass是典型的版本敏感依赖,Node版本偏高或偏低都会触发编译失败。Vue 2项目里如果package.json锁了node-sass版本,换一台电脑装依赖就要看运气。

解决:遇到node-sass报错,直接把package.json里的node-sass删掉替换成sass,重新npm install。Node版本建议锁定16.x或18.x,太新的Node 20对旧版Vue CLI和webpack有兼容问题。如果是Vue 2加Element UI的组合,Node 16是最稳的搭配,不建议用最新的LTS去试错。

5.3 跨域与登录失效问题

现象:前端页面能正常打开,但点任何按钮请求都报Access to XMLHttpRequest at 'http://localhost:8080' from origin 'http://localhost:5173' has been blocked by CORS policy,或者接口通了但一直弹重新登录。

原因:前后端分离项目两个服务端口不一样,浏览器同源策略直接把跨域请求拦了。登录失效一般是Session机制在前后端分离场景下的水土不服,后端Session存在自己的内存里,前端请求没带cookie或者后端校验逻辑异常,导致登录态丢失。

解决:跨域最简单的处理是后端写一个CORS配置类,允许前端来源地址访问;生产环境建议用nginx反向代理,把前端请求里的/api前缀转发到后端服务,这样前端访问的是同源地址,压根没有跨域这回事。登录失效问题先确认axios请求拦截器里token有没有正确放入header,再看后端过滤器是否放行了登录接口——很多源码的过滤器把登录接口也拦了,导致账号密码正确也进不去系统。

5.4 中文乱码:建库、连接串、表排序规则三步排查

现象:页面上录入中文保存后,数据库里显示???,或者反过来前端页面查出来全是乱码。

原因:数据库连接串没指定编码,或者建库时字符集用的还是latin1。MySQL默认字符集和连接字符集不一致,中文在写入和读取时就会在某个环节被转义成问号。

解决:三步全走一遍。第一步建库时指定CREATE DATABASE stock DEFAULT CHARACTER SET utf8mb4,第二步连接url末尾加characterEncoding=utf8,第三步检查表的排序规则是不是utf8mb4_general_ci。utf8mb4比utf8多支持了emoji等四字节字符,现在新项目建表直接用utf8mb4,别再用老旧的utf8了。

5.5 并发出库库存扣成负数

现象:两个人同时操作同一商品出库,库存剩10个,一个人出8个,另一个人出5个,最终库存变成-3。

原因:代码逻辑是典型的先查再改:先select查库存,应用层判断够不够,够了再update扣减。两步操作之间存在时间差,并发请求下两个线程都读到库存为10,都认为可以扣,后执行的那个update就把库存扣成了负数。

解决:用第4章那个带条件判断的update SQL,把quantity >= #{quantity}写进where条件里,数据库行锁保证同一时间只有一个事务能更新这一行。第二个人的update影响行数为0,Service层捕获到并发冲突,返回友好的失败提示。库存是资金级数据,扣减操作不能只看查出来的值,要用数据库层面的条件更新做最终保证。

6. 进阶:给这套系统接入JWT登录鉴权

很多库存管理源码登录用的是Session,但前后端分离部署时Session维护成本高,前端独立域名,后端无状态化才是长久之计。改造思路不复杂:登录成功后后端生成JWT返回给前端,前端存localStorage,每次请求在header里带上,后端写一个拦截器校验token有效性,登录接口放行,其余接口全部走校验。

// 登录成功生成token String token = Jwts.builder() .setSubject(username) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

这段代码需要引入jjwt依赖,signWith的密钥放在application.yml配置里。要注意jjwt的0.9.x和0.11.x版本API差异很大,网上大量教程还是老版本写法,0.11.x要求用Keys.hmacShaKeyFor(secretKey.getBytes())构建签名Key,直接复制老代码大概率报警告或异常。token过期时间设成3600000毫秒,也就是一小时,内部系统不需要太长。

后端写好拦截器后,用postman验证三个场景:不带token访问受保护接口返回401,带错误token返回401,带正确token正常拿到数据。三条过了再切到前端页面联调,确保axios响应拦截器对401的统一跳转登录逻辑生效。从那以后我每次接手带登录的Springboot源码,都会先看一遍过滤器链和token处理,确认无状态化之后才敢谈功能改造——登录这块的黑匣子不打开,后面所有接口调试都会被它卡住。希望帮到你。

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

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

AI应用不止Agent:跳出范式禁锢,重塑更自由的智能产品形态

先从一个我最近经常遇到的现象说起&#xff1a;你在技术社区里问“AI应用怎么做”&#xff0c;十个人里有九个会告诉你“先选一个Agent框架吧”&#xff0c;剩下一个推荐你用LangChain或者Dify。搜索词里也全是“agent开发”“agent框架”“agent安全”“agent anywhere”&…

作者头像 李华
网站建设 2026/10/27 16:49:08

企业智能体选型指南:Coze、Dify与LangChain如何抉择?

经常有人问我一个很实在的问题&#xff1a;企业想搭一个专属智能体&#xff0c;到底该用 Coze、Dify 还是 LangChain&#xff1f;问的人里面有做技术的&#xff0c;也有纯粹的业务负责人。他们真正纠结的不是“哪个工具更火”&#xff0c;而是“我搭出来的东西能不能符合业务预…

作者头像 李华
网站建设 2026/10/27 16:48:46

音乐推荐系统架构与深度学习模型实践

1. 项目背景与核心价值 音乐推荐系统在当今流媒体时代已成为刚需功能。根据行业数据显示&#xff0c;主流音乐平台用户日均播放量中&#xff0c;算法推荐产生的播放占比超过70%。传统协同过滤推荐算法面临冷启动、数据稀疏等问题&#xff0c;而结合大数据与深度学习技术的混合推…

作者头像 李华
网站建设 2026/10/27 16:48:22

网络安全漏洞信息收集全流程实战指南

1. 漏洞信息收集概述漏洞信息收集是网络安全领域的基础性工作&#xff0c;也是渗透测试和风险评估的首要环节。作为从业十余年的安全工程师&#xff0c;我见过太多项目因为前期信息收集不充分而导致后续测试效果大打折扣的情况。一个完整的漏洞信息收集流程&#xff0c;应该像侦…

作者头像 李华
网站建设 2026/10/27 16:48:01

肝病智能诊断系统:多模态融合与临床实践

1. 项目概述&#xff1a;肝病智能诊断系统的临床价值与技术路径肝病是全球范围内的高发疾病&#xff0c;每年导致超过100万人死亡。传统诊断依赖医生经验解读实验室指标和影像数据&#xff0c;存在主观性强、效率低下的痛点。这个毕业设计项目通过构建端到端的智能诊断系统&…

作者头像 李华
网站建设 2026/10/27 16:47:48

DESIGN.md:结构化设计规范提升AI设计输出质量

1. 项目概述&#xff1a;用DESIGN.md规范AI设计输出的革命性方案最近在设计圈里有个头疼的问题越来越明显——很多团队开始用AI工具生成UI设计稿&#xff0c;但结果往往惨不忍睹。上周我就遇到个典型例子&#xff1a;某创业团队用AI生成的登录页面&#xff0c;提交按钮居然放在…

作者头像 李华