简介:这是一份基于Spring+SpringMVC+MyBatis框架的鲜花销售管理系统完整工程,面向正在学习Java Web、需要SSM项目参考的学生或初级开发者。项目使用MySQL保存鲜花库存、订单与客户数据,前端集成LayUI、Bootstrap等样式与交互组件,涵盖商品管理、订单跟踪、用户管理、库存监控、销售统计等典型电商业务模块。压缩包约22.68MB,共637个文件,其中以Java源码(105个)、JSP页面(80个)、JavaScript脚本(98个)、CSS样式(63个)、SQL建表脚本及MyBatis映射文件为主,另有大量GIF/JPG图片资源用于界面展示,整体目录结构清晰,便于按模块查阅。截至目前已有3122人浏览学习。对希望快速上手SSM整合、理解前后端交互流程或基于现有工程完成课程设计的读者而言,是一套代码完整、可直接运行和二次开发的参考资料。
1. SSM项目鲜花销售管理系统:一个能直接跑通的全栈样本
SSM项目鲜花销售管理系统,名字听着像课设标配,实际拆开看,是一套Spring+SpringMVC+MyBatis整合得比较完整的Java Web后台项目。它把商品管理、订单管理、客户管理、库存管理和销售报表五个模块串在同一条业务线上,前端用LayUI配合Bootstrap、Font Awesome搭管理界面,MySQL负责数据存储。适合两类人:一类是刚学完SSM框架、想找个完整项目验证整合能力的开发者,另一类是做课设或毕设时需要参考业务闭环的同学。这份资源的价值不在代码量,而在于三层架构、事务控制、动态SQL、分页查询这些SSM核心点,都落在了一个能跑的鲜花业务场景里,拿来练手和改造都够用。
2. 技术栈拆解:SSM三层架构在鲜花业务里怎么协作
SSM的三层分工很多人能背,但一打开真实项目就发懵:Controller该写多少逻辑,Service为什么包了一层接口,Mapper XML里的SQL为什么不能用拼接。这套鲜花销售系统正好把三层的职责用业务串清楚了,下面按一个请求从浏览器进来的顺序拆。
2.1 请求进来之后:SpringMVC控制器层的分发路径
浏览器访问/flower/list这个地址,最先碰到的是web.xml里配置的DispatcherServlet。它把匹配的请求拦下来,交给HandlerMapping找到对应的Controller方法。系统里每个模块对应一个Controller,FlowerController负责鲜花商品,OrderController负责订单,CustomerController负责客户信息。
控制器层只做三件事:接收参数、调Service、把结果封装成JSON或ModelAndView返回。以商品列表为例,常见写法是这样的:
@Controller @RequestMapping("/flower") public class FlowerController { @Autowired private FlowerService flowerService; @RequestMapping("/list") @ResponseBody public LayuiResult list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String keyword) { PageInfo<Flower> pageInfo = flowerService.pageQuery(page, limit, keyword); return LayuiResult.success(pageInfo.getTotal(), pageInfo.getList()); } @RequestMapping("/add") @ResponseBody public Result add(@RequestBody Flower flower) { flowerService.addFlower(flower); return Result.ok("添加成功"); } }代码里的LayuiResult是专门为LayUI表格封装的返回结构,固定带code、msg、count、data四个字段,LayUI的table组件只认这个格式。@RequestParam(defaultValue = "1")给页码一个兜底值,前端不传page时默认查第一页,limit默认10条。@ResponseBody表示把方法返回值直接序列化成JSON,不走视图解析器。
注意Controller里不能出现SQL,也不该直接操作事务,这些是Service层的职责。判断一个Controller写得好不好,就看它能不能被快速读懂:方法名对应一个业务动作,参数直接映射前端请求,返回结构固定。这套项目里Controller基本都是这种瘦写法,照着这个习惯扩展新模块不会跑偏。
2.2 数据落库:MyBatis持久层与Mapper XML的映射细节
Controller往下调Service,Service再调Mapper接口。MyBatis这一层最核心的是Mapper接口和Mapper XML的对应关系——接口里写方法签名,XML里写SQL,两者通过namespace和方法id关联。
以FlowerMapper为例,查鲜花列表的SQL写在XML里,接口只留一个方法声明。分页用PageHelper插件,SQL里不用手写LIMIT,插件会自动在语句后面拼接分页参数:
<mapper namespace="com.flower.mapper.FlowerMapper"> <resultMap id="flowerMap" type="com.flower.entity.Flower"> <id property="id" column="id"/> <result property="flowerName" column="flower_name"/> <result property="price" column="price"/> <result property="stock" column="stock"/> <result property="categoryId" column="category_id"/> <result property="status" column="status"/> </resultMap> <select id="selectPage" resultMap="flowerMap"> select id, flower_name, price, stock, category_id, status from flower <where> <if test="keyword != null and keyword != ''"> and flower_name like concat('%', #{keyword}, '%') </if> </where> order by id desc </select> </mapper>resultMap做的是数据库下划线字段和Java驼峰属性的映射,flower_name对应flowerName,实体类里不用写一堆带下划线的字段名。<where>标签是动态SQL的实用写法:keyword为空时整个条件不生成,不为空时自动补上and开头的条件,避免手拼SQL多出个多余的where或and。
这里要强调#{}和${}的区别。#{}是预编译占位符,传值走PreparedStatement,能防SQL注入;${}是直接字符串替换,拿来拼表名、排序字段可以,拼用户输入就是给自己挖坑。这套系统里所有条件查询都用#{},算是SSM项目里比较规范的习惯。
2.3 业务组装:Spring依赖注入与事务边界
Service层是三层架构里最容易被写厚的一层,常见问题是把Controller的逻辑往下挪,在Service里堆几十行if-else。这套系统的Service拆得比较规矩:每个模块一个Service接口加一个ServiceImpl实现类,Controller只依赖接口,具体实现由Spring容器注入。
@Service public class FlowerServiceImpl implements FlowerService { @Autowired private FlowerMapper flowerMapper; @Override @Transactional(rollbackFor = Exception.class) public void addFlower(Flower flower) { flowerMapper.insert(flower); } @Override public PageInfo<Flower> pageQuery(Integer page, Integer limit, String keyword) { PageHelper.startPage(page, limit); List<Flower> list = flowerMapper.selectPage(keyword); return new PageInfo<>(list); } }@Service把实现类注册成Spring bean,@Autowired按类型注入FlowerMapper。分页查询有个细节必须注意:PageHelper.startPage(page, limit)必须放在mapper方法调用之前,并且紧跟它的第一条SQL才会生效。如果中间隔了别的查询,分页就串到错误的SQL上去了。
@Transactional(rollbackFor = Exception.class)是事务注解,rollbackFor指定了任何异常都回滚。如果不写这个参数,默认只在RuntimeException时回滚,SQLException这类受检异常不会触发回滚,数据就可能停在一种"半成功"状态。后面讲订单模块时,下单和扣库存必须在一个事务里,这个注解的作用会体现得很明显。
3. 环境搭建与导入:数据库脚本、IDEA配置与跑通前的检查清单
解压压缩包后,里面是一个标准的Web工程结构,带org.eclipse.wst.common.component这类Eclipse工程描述文件,同时也保留了Maven的pom.xml。这意味着有两条导入路径:Eclipse EE直接导入已有工程,或者IDEA里通过Maven打开。我更推荐IDEA,处理依赖和Tomcat部署更省心。跑通之前有几个前置步骤必须按顺序做,否则后面全是莫名其妙的报错。
3.1 先跑通MySQL建表脚本:五张核心表的字段设计
压缩包里database目录下有个建表脚本,常见命名是init.sql或flower_shop.sql,里面是整套系统的建表语句。先建库,再跑脚本,用命令行或者Navicat执行都行。核心业务表有五张:鲜花商品表、订单表、订单明细表、客户表、管理员表。
CREATE DATABASE IF NOT EXISTS flower_shop DEFAULT CHARSET utf8mb4; USE flower_shop; CREATE TABLE flower ( id INT PRIMARY KEY AUTO_INCREMENT, flower_name VARCHAR(64) NOT NULL COMMENT '鲜花名称', price DECIMAL(10,2) NOT NULL COMMENT '销售单价', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', category_id INT COMMENT '分类ID', image_url VARCHAR(255) COMMENT '商品图片', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='鲜花商品表';提示:跑数据库脚本前先确认MySQL版本。5.7以下对utf8mb4和ON UPDATE CURRENT_TIMESTAMP支持不完整,建议用5.7或8.0;如果脚本里带了外键约束,注意先建主表再建子表,否则会报找不到表的错误。
五张核心表的职责边界如下:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| flower | flower_name, price, stock, category_id, status | 鲜花商品主数据 |
| orders | order_no, customer_id, total_amount, status, create_time | 订单主表,一个订单一条记录 |
| order_detail | order_id, flower_id, quantity, price | 订单明细,一个订单多条 |
| customer | username, password, phone, address | 注册客户信息 |
| admin | username, password, real_name | 后台管理员账号 |
orders表是业务核心,total_amount冗余存了订单总金额,避免每次统计都去明细表现算。这是很常见的表设计思路:汇总数据冗余存储,查询快,但更新时要保证一致。这套系统里订单金额在创建订单时计算并写入,后续不修改,所以冗余是安全的。
3.2 导入IDEA:Maven依赖与项目结构核对
脚本跑通后,打开IDEA,File -> Open,选择项目根目录下的pom.xml,IDEA会按Maven工程导入。第一次加载会自动下载依赖,耗时长短取决于网络状况。如果下载失败,多半是Maven仓库镜像源的问题,检查一下settings.xml里配置的镜像地址。
项目核心依赖集中在pom.xml里,打开后重点检查这几组坐标:
<dependencies> <!-- Spring + SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.30.RELEASE</version> </dependency> <!-- MyBatis 与 Spring 整合插件 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.2.1</version> </dependency> </dependencies>这组版本是SSM项目里很常见的一套组合:Spring 4.3配MyBatis 3.4,驱动用5.1.47。如果本机MySQL是8.0以上,驱动版本最好换成8.0系列的,否则连接时可能报SSL或认证协议不兼容。分页插件PageHelper需要单独引坐标,同时还要在MyBatis配置里注册对应的拦截器,少一步分页就不生效。
导入后注意一个点:如果项目结构里没有src/main/java这种Maven标准目录,而是Eclipse那种扁平的src目录,需要在Project Structure里手动把目录标记成Sources和Resources,否则IDEA不认这些源码目录,编译时全是红色的报错。
3.3 配置文件核对:数据源、视图解析器与静态资源映射
项目跑不起来,八成问题出在配置文件。SSM项目至少要核对四个文件:web.xml、spring-mvc.xml、spring-mybatis.xml、jdbc.properties。其中jdbc.properties最容易被改错,它决定了数据库能不能连上。
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456url里的flower_shop要和建库时的库名完全一致。characterEncoding=utf8是给中文数据兜底,防止存入数据库变成问号。useSSL=false在MySQL 5.7和8.0下能省掉一堆警告日志,本机开发完全够用。
spring-mvc.xml里要检查两个配置:视图解析器是否指向了正确的jsp目录,静态资源映射是否放行了CSS和JS。这套系统的管理界面引用了layui.css、bootstrap.min.css、font-awesome.css和animsition.css,如果静态资源被SpringMVC的前端控制器拦截,这些文件会全部404,页面样式全丢。常见的放行写法是:
<mvc:resources mapping="/static/**" location="/static/"/> <mvc:annotation-driven/>如果项目里静态资源直接放在webapp根目录下,就调整mapping路径,或者在web.xml里给静态文件单独配一个servlet映射。总之,前端文件能被浏览器直接访问到,页面才可能正常渲染。
4. 核心模块代码走读:商品管理、订单状态与库存扣减的细节
跑通之后,下一步是真正读懂业务代码。这套系统的业务线很清晰:管理员维护商品,客户下单,订单创建时扣库存。三个模块串起来就是一个完整交易闭环,下面按这条线的顺序过一遍关键代码。
4.1 商品管理:从Controller到Mapper的完整CRUD链路
商品管理是系统里最简单也最完整的CRUD模块,适合作为第一个仔细读的模块。FlowerController里增删改查四个方法分别对应四个Service方法,再往下是四个Mapper方法。以修改商品为例,链路是:前端表单提交 -> Controller接收Flower对象 -> Service调用update -> Mapper执行update语句。
@Controller @RequestMapping("/flower") public class FlowerController { @RequestMapping("/edit") @ResponseBody public Result edit(@RequestBody Flower flower) { int rows = flowerService.updateFlower(flower); if (rows == 0) { return Result.error("商品不存在或未变更"); } return Result.ok("修改成功"); } @RequestMapping("/delete") @ResponseBody public Result delete(Integer id) { int rows = flowerService.deleteFlower(id); if (rows == 0) { return Result.error("删除失败"); } return Result.ok("删除成功"); } }这里两个细节值得注意。第一,edit方法返回的rows是MyBatis执行update语句后返回的影响行数,如果传入的id在表里不存在,影响行数是0,可以借此给前端一个准确提示。第二,delete接口接收的是Integer id,前端传id即可,不需要把整个对象传回来。
实际项目里删除商品通常不做物理删除,而是逻辑删除,把status字段改成0让商品下架。这套系统里两种方式都出现过:物理删除适合清理测试数据,逻辑删除适合线上业务。读者在自己扩展时,建议默认用逻辑删除,商品一旦被订单引用,物理删除会把历史订单的明细变成孤儿数据。
4.2 订单管理:状态机字段与下单事务的实现
订单模块是整个系统里事务最集中的地方。创建订单时要写orders表、写order_detail表、再扣减flower表的库存,这三步必须保证要么全成功要么全回滚,否则会出现订单建了但库存没扣、或者库存扣了订单却没生成的脏数据。
订单状态用一个TINYINT字段存,不同数值代表不同阶段:
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | 待付款 | 刚创建,未支付 |
| 1 | 已付款 | 支付完成,待发货 |
| 2 | 已发货 | 卖家已发货,待收货 |
| 3 | 已完成 | 买家确认收货 |
| 4 | 已取消 | 超时或主动取消 |
状态流转的代码在OrderServiceImpl里,下单方法的典型实现是这样:
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private FlowerMapper flowerMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean createOrder(Order order, List<OrderDetail> details) { // 生成订单号并写入订单主表 order.setOrderNo(generateOrderNo()); order.setStatus(0); orderMapper.insert(order); // 逐条写明细,同时扣减库存 for (OrderDetail detail : details) { detail.setOrderId(order.getId()); orderDetailMapper.insert(detail); int rows = flowerMapper.deductStock(detail.getFlowerId(), detail.getQuantity()); if (rows == 0) { throw new RuntimeException("库存不足,订单创建失败"); } } return true; } }createOrder方法上的@Transactional(rollbackFor = Exception.class)是整个方法的事务边界。方法内任何一步抛出异常,orders表和order_detail表写入的数据连同库存扣减一起回滚。注意顺序:先生成订单号、写主表,再循环写明细并扣库存。如果把库存扣减放在订单主表写入之前,一旦主表写入失败,库存却被扣了,逻辑上虽然事务回滚能恢复,但放在明细循环里更直观,容易看出一单对应几条明细。
订单号生成一般用时间戳加随机数组合,避免并发下单撞号。generateOrderNo()内部常见做法是拼接yyyyMMddHHmmss加几位随机数,再加一个自增序列,这样同一秒内也不会重复。
4.3 库存扣减:一个防超卖的条件更新写法
库存扣减是秒杀和电商场景里的经典问题。朴素的写法是先select查一次库存,判断够不够,再update扣减。但两个操作之间有时间窗口,并发请求下可能都查到库存是5,然后都去扣,最终库存变成负数——这就是超卖。
更稳妥的做法是把判断和扣减合并成一条SQL,用条件更新保证原子性:
update flower set stock = stock - #{quantity} where id = #{flowerId} and stock >= #{quantity}这条SQL的执行过程是:只更新那些当前库存不小于购买数量的记录。如果库存不足,影响行数返回0,Java代码里通过rows == 0就能判断并抛异常,触发事务回滚。整个过程没有先查后改的间隙,也就没有并发超卖的问题。
MySQL的InnoDB引擎在执行update时会对命中行加锁,stock >= #{quantity}这个条件在判断时已经处于锁定状态,所以即使两个请求同时到达,也会一个成功一个失败,而不是同时成功。这个写法比select+update的朴素写法可靠得多,也是这套系统订单模块里的默认做法。
要注意的是,这种条件更新依赖数据库的行锁,前提是where条件能命中索引。flower表主键是id,按id更新天然走主键索引,锁粒度最小。如果改成按flower_name去扣库存,表扫全表时会锁更多的行,并发性能明显下降。实际项目中扣库存的SQL一律以id为条件,这是约定俗成的习惯。
5. 避坑与排查:SSM项目导入后跑不起来的五个经典问题
这一节是实操里最容易卡住的地方,全部按"现象 -> 原因 -> 解决"的顺序写,遇到相同问题可以直接对号入座。
5.1 Tomcat启动报ClassNotFoundException:依赖没进lib目录
现象:启动Tomcat,控制台直接抛ClassNotFoundException: org.springframework.web.context.ContextLoaderListener,或者NoClassDefFoundError,项目里所有Spring相关的类都找不到。
原因:Maven依赖根本没有被部署到Tomcat的lib目录。IDEA里导入Maven项目后,依赖虽然在External Libraries里能看到,但如果Artifacts配置里没有把Maven依赖打进去,Tomcat运行时classpath里就没有这些jar包。从Eclipse工程转过来的项目尤其容易踩这个坑,Artifacts类型经常是遗留的Exploded形式,依赖列表是空的。
解决:Project Structure -> Artifacts -> 选择该项目 -> 在Available Elements里把Maven依赖全部右键Put into WEB-INF/lib。更省事的做法是右键项目 -> Add Framework Support -> 重新关联Maven,让IDEA自动生成包含依赖的Artifacts,操作完重启Tomcat。
5.2 数据库连接失败:数据源参数与MySQL版本不匹配
现象:Tomcat起来了,但一访问列表页就报错,日志里出现Access denied for user 'root'@'localhost',或者Communications link failure。
原因:Access denied是账号密码不对,或者root账号只允许特定主机登录。Communications link failure通常是MySQL服务没启动、端口不是3306、或者驱动和数据库版本不匹配。驱动5.1.47连MySQL 8.0经常报SSL握手失败或Public Key Retrieval错误。
解决:先确认MySQL服务在运行,命令行能连上。再把jdbc.properties里的用户名密码改成可用的。如果本机是MySQL 8.0,驱动换成mysql-connector-java 8.0.x,url里追加useSSL=false和allowPublicKeyRetrieval=true,后一个参数解决的是8.0版本在非SSL连接下需要公钥的问题。改完配置必须重启Tomcat,properties文件不会热加载。
5.3 静态资源全部404:SpringMVC拦了CSS和JS
现象:页面能打开,HTML结构在,但layui.css、bootstrap.min.css、animsition.min.css、font-awesome.css这些文件全部404,页面没有样式,整个是裸的。
原因:SpringMVC的前端控制器把静态请求也拦截了。web.xml里DispatcherServlet的url-pattern配成/或者/*,所有请求都会先经过SpringMVC,而配置里没有放行静态资源。系统里引用的bootstrap.min.css、animsition.css这些文件路径如果写的是相对路径或带/vendor/前缀,更容易踩这个坑。
解决:在spring-mvc.xml里加<mvc:resources mapping="/static/**" location="/static/"/>,同时保证页面里引用的路径和resources目录结构一致。改完还是404的话,F12看网络请求,确认实际请求的URL路径,对照文件在项目里的真实位置调整mapping。土办法是在web.xml里给css、js、png单独配DefaultServlet映射,但尽量不用,mvc:resources更干净。
5.4 接口返回的JSON中文乱码:编码处理缺失
现象:Ajax请求列表数据,浏览器里中文全部变成问号或乱码,数字和英文正常。
原因:SpringMVC的消息转换器默认编码不是UTF-8。@ResponseBody返回的对象由MappingJackson2HttpMessageConverter序列化,如果整个应用没有统一UTF-8编码,返回的JSON就是ISO-8859-1编码,浏览器解析时自然乱。另外数据库连接url里没配characterEncoding=utf8,查询结果也可能乱,这两种要分开排查。
解决:spring-mvc.xml里配<mvc:annotation-driven>时指定消息转换器编码,或者在web.xml里配CharacterEncodingFilter,强制所有请求和响应走UTF-8。更直白的做法是在@RequestMapping里加produces = "application/json;charset=utf-8",建议优先用全局级别的CharacterEncodingFilter,省得每个接口都声明一次。
5.5 LayUI表格空白:返回格式与table组件不匹配
现象:LayUI的table.render拿到数据后,表格一直是空的,但Network里接口返回正常,数据也看得到。
原因:LayUI table组件对返回数据格式有严格要求:必须是一个对象,包含code、msg、count、data四个字段,code必须是0,data必须是数组。如果Controller返回的包装类code是200,或者其他字段名不匹配,LayUI就不认,自动当成空数据处理。系统里封装了LayuiResult专门解决这个问题,但二次开发时如果用了普通Result包装类,表格立刻空白。
解决:检查接口返回的JSON结构,确认是{"code":0,"msg":"","count":100,"data":[...]}这样的格式。查一下Controller返回值用的是LayuiResult还是普通Result。另外table.render里page的limit要和Controller的limit参数保持一致,否则会出现页码点不动或显示条数异常。
6. 进阶实践:把报表统计模块从"能看"改成"能用"
报表统计往往是SSM项目里最不起眼的模块,但真实销售场景中,老板盯得最多的恰恰是它。这套系统预留了报表统计的功能位,具体实现需要自己补。日报表和库存预警是性价比最高的两个方向,SQL都不复杂,关键是过滤条件和展示形式的细节。
6.1 日销售额与畅销商品的统计SQL
统计日销售额,核心是对orders表按天分组。orders里有create_time、total_amount和status字段,过滤条件要排除已取消的订单,否则取消单会把销售额虚高:
select date(create_time) as day, count(*) as order_count, sum(total_amount) as total_amount from orders where status in (1, 2, 3) and create_time >= date_sub(curdate(), interval 30 day) group by date(create_time) order by day descdate(create_time)把datetime截断成日期,30天窗口由date_sub动态生成,不用在Java里拼日期字符串。畅销商品榜则用order_detail关联flower表,按flower_id分组后sum(quantity)降序取前10,写法同理。
6.2 用一个完整流程验证系统是否真的跑通
代码读到这,建议做一次端到端验证,不是看页面能打开,而是走一条完整交易链路:登录管理员,新增一个鲜花商品并设置价格和库存;去前台以客户身份下单;回后台确认订单出现且状态是待付款;再回商品列表确认库存已扣减。最后打开报表,看今天的销售额统计是否包含这笔订单。
这五步全通过,说明三层架构、事务、MyBatis动态SQL、分页、前端表格这些核心环节真正跑通了。我拿到这类SSM项目后,会先做全链路验证再读源码。数据库脚本和配置参数看着再正常,都要以这条链路的结果为准。有次我接手项目,页面能访问但一下单就报库存异常,排查半天发现是order_detail的quantity字段是INT,前端却传了字符串,MyBatis参数类型匹配出了偏差。这种问题只有走完整链路才暴露得出来。
从那以后,我每次拿到Java Web项目,第一件事永远是做一遍从建库到下单的完整验证,有了基准线再谈改造。这套鲜花销售管理系统也是一样,先跑通流程,再改成自己的业务,希望帮到你。
本文还有配套的精品资源,点击获取