ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM汽车销售系统开发实战:从数据库设计到权限控制全解析

SSM汽车销售系统开发实战:从数据库设计到权限控制全解析 1. 这个项目解决的是什么问题汽车销售系统的真实业务场景我最早接到这个题目的时候第一反应其实是又一个培训班或者毕业设计项目。但真正着手整理的时候发现这类系统远没有表面看起来那么简单——SSMSpring SpringMVC MyBatis架构下的汽车销售系统本质上是典型的进销存加客户关系管理的复合型业务系统汽车销售这四个字背后覆盖了商品管理、库存流转、订单生成、客户跟进、财务统计这些在真实企业里都要打通的环节。这个java_ssm59汽车销售系统编号我之前也查过应该是某套课题库里的项目序号第59号。很多同学拿了这个题目之后第一反应是又是CRUD结果写着写着就发现光是把一辆车从入库到卖掉到出库再到统计销售业绩这条链路理清楚就已经能劝退一批人。所以这篇文章不是给你贴一段代码让你抄而是把我做这个项目的完整思路、踩过的坑、以及怎么把它讲出彩的经验全部拆开。不管你是拿来交课程设计、应付毕业答辩还是想在简历上写一个拿得出手的SSM项目这篇都能帮你省下至少一周的瞎折腾时间。先说这个项目到底要做哪些事。一台车进入4S店的销售体系背后至少涉及四条业务线车辆管理线车辆进货入库、库存管理、车辆信息维护品牌、型号、颜色、配置、价格、车辆状态流转在库、已预订、已售出客户管理线潜在客户登记、意向车型记录、跟进状态更新、成交客户归档销售业务线销售订单创建、合同信息录入、付款方式管理、车辆出库统计分析线按车型、按销售员、按时间段统计销量和销售额生成简单的报表。如果你只做一个简单的增删改查那这个项目做完也就值一个及格分。真正要让老师或者面试官眼前一亮核心在于把汽车销售场景里的业务约束落到代码里比如库存为0时不允许下单、同一辆车不能重复销售、销售订单必须关联到具体的客户和销售员。这些约束才是区分背代码和做项目的分水岭。1.1 为什么我选择SSM组合而不是更时尚的Spring Boot我知道看到这里肯定有人会问现在出去找工作大家都用Spring Boot为什么课程设计还要用SSM这不是老掉牙吗我当时的判断是这样的这个项目的要求就是SSM那SSM恰恰是最能帮你理解Java Web底层原理的组合。Spring Boot帮你把一切自动化配置好了你启动一个项目可能一行XML都不用写。但SSM不一样它逼着你手动去处理Spring容器的配置文件、SpringMVC的拦截器配置、MyBatis的映射文件你必须清楚每一个Bean是怎么被创建出来的、每一次请求是怎么被DispatcherServlet分发到Controller的、每一次数据库操作是怎么通过SqlSession执行到Mapper接口的。举个最直白的例子。你用Spring Boot的时候写一个Mapper注解就完事了。但在SSM项目里你得自己写mybatis-config.xml还要在Spring的配置文件里用MapperScannerConfigurer去扫包才能把Mapper接口动态代理成Bean。这个过程你真的跑通一遍以后面试官问你MyBatis的Mapper接口为什么不需要实现类你根本不用背答案因为你见过那个代理对象是怎么来的。而且SSM项目在课程设计这个场景下有个隐形优势它看起来更像一个系统。Spring Boot项目一个页面加一个接口开发速度确实快代码结构也简单但答辩的时候你能讲的东西也少了。SSM项目天然有web.xml、Spring配置、SpringMVC配置、MyBatis配置这四层结构每一层都能拆出知识点来讲凑项目篇幅和答辩时间都更容易。当然如果你已经熟练SSM非要在这套项目里引入Spring Boot也不是不行。但我的建议是既然课题名字写的是SSM就老老实实按SSM做答辩的时候老师问什么你都能接得住。1.2 模块边界划分从销售流程反推系统功能做系统最忌讳的事情是拿到需求就打开Navicat建表然后开始无脑写CRUD。做过两个以上项目你就会发现模块划分是否合理直接决定这个项目后期开发是越来越顺还是越来越乱。我习惯的做法是从业务流程图倒推。画出汽车销售的闭环市场活动吸引客户 → 客户到店/线上咨询 → 销售顾问登记客户信息 → 客户看车谈价格 → 成交签订销售订单 → 财务收款 → 车辆出库交付 → 售后回访。把所有环节摊开系统的功能模块自然就浮出来了系统登录与权限模块管理员和销售员两种角色管理员能看所有数据销售员只能看到自己名下的客户和订单车辆信息管理模块车辆的品牌、车型、配置参数、指导价、库存数量以及车辆进货入库和销售出库的流水记录客户信息管理模块客户基本资料、联系电话、意向车型、关注等级、跟进记录销售订单模块创建订单时关联客户和车辆记录成交价格、付款方式全款/分期/置换、订单状态统计分析模块按时间范围统计各车型销量、各销售员业绩算出总销售额。我第一次做这个项目的时候就犯了一个典型的错误一上来就把所有功能塞到一张菜单里车辆管理、订单管理、客户管理之间颗粒度混乱订单列表里塞了一堆车辆字段。后来我痛定思痛重新画了流程图把模块拆干净后端的Controller和Service层瞬间清爽了很多。这里我明确给一个建议不要在一开始就追求功能大而全。先把核心流程跑通再逐步加功能。汽车销售系统最核心的三件事就是车辆、客户、订单这三张表之间的关系打通了系统的主体骨架就成立了。至于那些花里胡哨的图表、短信通知、报表导出都属于锦上添花后期有余力再加。2. 数据库表设计这个项目成败的关键一步很多人做JavaWeb课程项目对数据库设计的重视程度远远不够。他们觉得反正就是增删改查能查出数据就行。但实际上数据库表设计的好坏决定了你整个项目代码的复杂度。同样的业务表设计合理Service层可能只要几十行代码表设计不合理一条查询你要join五张表Mapper的SQL写得跟天书一样自己二天回来看都不知道写的是什么。我建表有一个铁律先梳理业务中出现的名词和动词。名词通常是实体动词通常是实体之间的行为。汽车销售系统里的名词有用户管理员/销售员、车辆、客户、订单。动词有入库、售出、跟进、统计。把名词和动词组合起来表结构自然就出来了。2.1 六张核心表的字段设计与关系梳理我这个项目最终落地的核心表一共六张每一张的字段我都调整过至少两轮你们可以直接参考根据自己的需求增减字段。先说用户表sys_user这张表最简单但最容易忽略权限字段CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role INT NOT NULL COMMENT 1-管理员 2-销售员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有个细节值得说一下password字段长度我特意留了100位。原因很简单数据库里存的绝对不能是明文密码我用了MD5加盐的方式处理。就算只是一个课程设计也建议养成这个习惯面试的时候这属于亮点细节。然后是车辆品牌表car_brand和车辆信息表car_info。为什么品牌要单独拆一张表而不是直接在车辆信息表里写一个brand_name字符串字段因为品牌是一个有限集合拆出来既方便后台维护品牌列表也方便按品牌做统计。这是非常典型的数据库设计思维——把属性升级为实体。CREATE TABLE car_brand ( id INT PRIMARY KEY AUTO_INCREMENT, brand_name VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE car_info ( id INT PRIMARY KEY AUTO_INCREMENT, brand_id INT NOT NULL, model_name VARCHAR(100) NOT NULL COMMENT 车型名称, color VARCHAR(30), config_level VARCHAR(50) COMMENT 配置等级低/中/高/顶配, guide_price DECIMAL(10,2) COMMENT 指导价, stock_quantity INT DEFAULT 0 COMMENT 库存数量, status INT DEFAULT 0 COMMENT 0-在售 1-停售, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (brand_id) REFERENCES car_brand(id) );**客户表customer和销售订单表sale_order**则要体现业务关系CREATE TABLE customer ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, gender VARCHAR(10), intention_model VARCHAR(100) COMMENT 意向车型, follow_up_status INT DEFAULT 0 COMMENT 0-待跟进 1-跟进中 2-已成交 3-已流失, sales_user_id INT COMMENT 负责销售员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50) NOT NULL UNIQUE COMMENT 订单编号, customer_id INT NOT NULL, car_info_id INT NOT NULL, quantity INT DEFAULT 1 COMMENT 购买数量汽车销售通常是1, actual_price DECIMAL(10,2) COMMENT 成交价格, payment_type INT COMMENT 1-全款 2-分期 3-置换, sales_user_id INT COMMENT 销售员, order_status INT DEFAULT 0 COMMENT 0-待付款 1-已付款 2-已完成 3-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customer(id), FOREIGN KEY (car_info_id) REFERENCES car_info(id) );最后一章是车辆入库流水表stock_log。这个表可能不是必需的表但加上它会让整个项目直接上升一个档次。它记录了每台车什么时候入库、什么时候出库是走销售订单卖掉的还是内部调拨的。有了这张表你就能回答面试官怎么查某段时间进销存数据这种问题。2.2 外键到底建不建很多人的认知是错的数据库设计极易引爆争议的一个问题就是MySQL到底要不要用外键网上的主流论调是互联网大厂都不用外键因为外键影响并发性能和拓展性。这话本身没错但它有个前提——那是针对超高并发、超大流量的互联网业务。你这个汽车销售系统是个什么量级一天几千单顶天了而且业务本身强关联客户、订单、车辆之间的引用关系不允许断裂。用外键完全没毛病甚至还可以享受数据库层面的完整性保障。我记得有一次测试的时候我故意删掉一个已经被订单引用的车辆品牌记录数据库立刻报错拒绝删除那一刻我是松了口气的。如果没有外键约束这个错误会在运行期悄悄变成一条不可解释的业务Bug排查起来非常头疼。所以我给这个项目的取舍是核心的业务关联订单-客户-车辆建外键保证一致性非核心关联用逻辑关联比如stock_log表里的order_id字段我只存值不建外键因为流水表是追加写入的不需要被引用。另外还有一个容易翻车的地方MySQL中MyISAM引擎不支持外键。早年间很多教程用MyISAM作为默认引擎到了5.7以后才默认换成InnoDB如果你在之前的旧项目基础上改一定要用SHOW TABLE STATUS确认表的引擎是InnoDB。3. 核心业务实现与踩坑记录代码不是写出来就完事的这是整篇文章的硬核部分。SSM项目的框架搭建网上教程一搜一大把我不重复了。真正值得花篇幅写的是那些业务逻辑上的暗坑这些都是你光看视频教程学不到的。3.1 库存扣减与事务边界一个看似简单的业务逻辑下单卖车听起来简单得不能再简单创建订单扣库存。但落到代码里就会出现经典问题——如果扣库存成功了但创建订单失败了会发生什么我拿这个问题问过几个同样做这个项目的朋友一半以上第一反应回答不出来。很简单数据库会告诉你答案库存扣减是UPDATE语句订单创建是INSERT语句。如果这两条SQL不在同一个事务里那么UPDATE执行成功后系统报错库存就莫名其妙少了一台可订单根本不存在。这就是典型的数据不一致。正确做法是在Service层加上Transactional注解让这两步操作绑定同一个事务要么都成功要么都回滚Service public class SaleOrderServiceImpl implements SaleOrderService { Autowired private CarInfoMapper carInfoMapper; Autowired private SaleOrderMapper saleOrderMapper; Override Transactional(rollbackFor Exception.class) public boolean createSaleOrder(SaleOrderVO vo) { // 1. 先校验车辆库存 CarInfo carInfo carInfoMapper.selectById(vo.getCarInfoId()); if (carInfo null || carInfo.getStockQuantity() 0) { throw new BusinessException(车辆库存不足); } // 2. 扣减库存 int updateRows carInfoMapper.deductStock(vo.getCarInfoId(), 1); if (updateRows 0) { throw new BusinessException(扣减库存失败车辆可能已被售出); } // 3. 创建订单 SaleOrder order new SaleOrder(); // ... 生成订单号填充字段 int insertRows saleOrderMapper.insert(order); if (insertRows 0) { throw new BusinessException(订单创建失败); } return true; } }这里我再强调一个很多人忽略的Transaction的细节Spring的Transactional默认只对RuntimeException回滚对受检异常Exception的子类但不继承RuntimeException不回滚。考试和面试都爱问这个点。为了保险我用的是rollbackFor Exception.class让所有异常都触发回滚。还有一个性能上的点。扣减库存的正确SQL不是先查一遍库存数量再比较而是用乐观锁的思路直接扣通过受影响的行数判断是否失败UPDATE car_info SET stock_quantity stock_quantity - 1 WHERE id #{carId} AND stock_quantity 0;为什么不能先SELECT再UPDATE因为这里的判断和更新之间隔着一个网络往返如果两个用户同时下单都查到了库存为1两个判断都通过两条UPDATE都执行库存就变成-1了。而上面这种写法把判断库存是否足够和扣减库存合并到一步原子操作里数据库层面的行锁保证了不会超卖。这个细节是我踩过坑之后才真正理解的。3.2 整数类型选错引发的金额隐患Decimal与Double的生死抉择这个坑我相信很多做订单系统的人都踩过但我还是要专门拿出来说凡是涉及金额、价格、折扣的字段一律使用DECIMAL类型绝对不能用FLOAT或DOUBLE。你可能会觉得不可思议Float不行吗我在展示价格的时候明明看着挺正常的。问题出在计算上。浮点数在计算机中是用二进制表示的很多十进制小数根本无法被精确表示。如果你用DOUBLE存成交价再用它做求和、算毛利率经过多次运算之后就会出现199999.99这种离奇数字。而DECIMAL是按字符串方式存储和计算的天然就是精确的。顺便说一句这不止是表设计的问题前端展示同样要注意。我用的是decimal类型的Java类字段和BigDecimal对应运算的时候也用BigDecimal而不是普通的加减乘除。你还别说BigDecimal也有一个著名的坑BigDecimal a new BigDecimal(0.1); BigDecimal b new BigDecimal(0.2); System.out.println(a.add(b)); // 输出 0.30000000000000000000000000000000004看到了吗new BigDecimal(0.1)其实用的是浮点字面量0.1的二进制近似值照样是不精确的。正确做法是用字符串构造器BigDecimal a new BigDecimal(0.1); BigDecimal b new BigDecimal(0.2); System.out.println(a.add(b)); // 输出 0.3做这个项目的时候我专门把工具类写好了金额一律用字符串入参运算结果统一保留两位小数。3.3 客户电话的唯一性校验一个容易忽略的业务约束这是个小细节但老师特别喜欢在答辩的时候问客户是不是同一个人怎么判断如果你没有约束同一个客户可以录三次电话号码被三个不同的销售员认领最后就会出现业绩归属纠纷真实业务场景里这叫飞单。我的做法是加一个SQL层面的唯一约束ALTER TABLE customer ADD UNIQUE INDEX uk_phone (phone);然后插入前的校验在Service层做Customer existing customerMapper.selectByPhone(vo.getPhone()); if (existing ! null) { throw new BusinessException(该电话已存在客户档案请勿重复录入); }这里有个看似简单、实际上还需要斟酌的点如果同一个客户想买第二辆车怎么办如果电话唯一索引死死定住那这个客户就不能再录一次了。真实的4S店里一个客户确实可能有多台购车记录但客户档案应该是归档在同一个ID下面的。所以最终我的设计是客户表只存客户基本资料一个客户电话唯一订单表外键关联客户ID。一个客户可以有多条订单这是标准的一对多关系。这个设计也让我在后面做客户忠诚度分析的时候省了很多事。4. 权限控制与登录模块从基础到进阶的两种实现登录模块是每个JavaWeb系统都有的模块但课程项目里能做出差异化的不多。我见过太多项目登录就是一个用户表按用户名密码查询查到了就跳转查不到就提示失败。这样做没什么错但也没有任何可以讲的亮点。我把权限控制分成了两个重要层面来讲一个是功能权限控制谁能访问哪个页面和接口另一个是数据权限控制一个人能看到哪些数据行。4.1 基于拦截器和Filter的访问控制实现我在SSM阶段做的权限控制是基于SpringMVC的Interceptor拦截器。它的工作方式可以理解成一个门卫在所有请求到达Controller之前先拦截下来检查当前用户的Session里是否有登录记录以及这个用户拥有的角色是否有权限访问当前请求的URL。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); SysUser user (SysUser) session.getAttribute(loginUser); if (user null) { // 未登录跳转到登录页面 response.sendRedirect(request.getContextPath() /login); return false; } // 用role字段做URl级别的简单权限判断 String uri request.getRequestURI(); if (uri.contains(/admin/) user.getRole() ! 1) { response.sendError(403); return false; } return true; } }这段逻辑很直白。但我不建议你直接在拦截器里堆太多判断可以把权限规则抽取成一个配置表就是常说的RBAC基于角色的访问控制用户-角色-权限三级模型。如果项目时间允许可以用一个简单的permission表存URL地址把一个URL授权给多个角色最终在拦截器里判断当前用户的所有角色中是否存在访问该URL的权限。这里顺便给大家排一个雷SpringMVC拦截器只拦截经过DispatcherServlet的请求不拦截静态资源路径。如果你的项目里出现登录页面的CSS样式加载不出来或者未登录居然能赤裸裸访问图片文件这类问题多数情况下就是没有在Spring的配置里加mvc:resources或mapping配置来排除静态资源路径或者反过来忘了放行静态资源。你要检查实现。4.2 数据权限销售员只能看到自己的客户先给各位看一下一个典型的Session存储用法。在用户登录成功后我把整个SysUser对象放进了Session而不是只存一个用户ID这样后续的Controller和Service可以直接取到用户角色和真实姓名避免每次都要查一次数据库。RequestMapping(value /login, method RequestMethod.POST) public String login(String username, String password, HttpSession session, Model model) { SysUser user userService.login(username, md5Password(password)); if (user null) { model.addAttribute(errorMsg, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); if (user.getRole() 1) { return redirect:/admin/index; } else { return redirect:/sales/index; } }登录功能不只是校验密码还要防止匿名用户直接跳转URL进入后台页面。这就是上面拦截器的意义。而数据权限的实现无论是从技术含量还是从面试角度来说都比功能权限更值得你做。场景很常见销售员登录系统后客户列表只能看到负责销售员是自己的那几个客户不能看到全公司的客户而管理员可以看全部。这就导致同一条Mapper查询方法普通写法是固定的SQL语句但实际业务需要根据登录人的身份来动态拼接WHERE sales_user_id 当前登录用户ID。MyBatis的动态SQL在这里就派上用场了select idselectCustomerList resultTypeCustomer SELECT * FROM customer where if testuserId ! null and role ! 1 AND sales_user_id #{userId} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /select意思很明确查客户列表的时候如果是销售员就在SQL后面强制拼上只能看本公司自己名下客户的过滤条件作为数据范围的控制管理员不加条件看全部。把这个讲清楚之后评委老师基本都会点头。5. 部署环境配置与经典报错排查你的最后一次翻车可能就在这里SSM项目跟SpringBoot项目最大的不同从环境配置这一点就能看出来。SpringBoot给你做了一站式依赖管理版本冲突的概率小得多。SSM则需要你自己处理JDK、Maven、Tomcat、框架版本之间的兼容性。每一个版本的不一致都可能在下一次启动时给你一个惊喜让你缴费。总结我这套项目里遇到过的几个最经典报错场景。5.1 JDK版本和Maven编译级别不一致的连锁反应这个报错很有意思不同的电脑上表现会完全不同。有的同学报错信息是java: 错误: 不支持发行版本 17有的是[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin...还有的是ClassNotFoundException: javax.servlet.ServletException。问题的本质都一样编译代码的JDK版本高于运行时Tomcat支持的JDK版本或者Maven默认的编译级别和项目本身的目标编译级别不一致。比如你的机器装的JDK 17但项目按照编译级别1.8来配置Maven编译阶段就会报不支持发行版本。我的建议非常统一在此统一规定为JDK版本安装JDK 81.8Maven编译插件的source和target都设置成1.8Tomcat版本选择Tomcat 8.5或Tomcat 9这两个兼容JDK 8和Servlet 3.1标准在pom.xml里显式配置编译插件build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.8.1/version configuration source1.8/source target1.8/target encodingUTF-8/encoding /configuration /plugin /plugins /build如果你的完美环境里就是喜欢高版本的JDK那也不是不能跑。但你得很愚笨地从各种框架配置文件中排查一切跟版本相关的隐患付出的代价远超想象。这是真实经验——一个大版本号的差异足够耗掉你一个下午。5.2 Tomcat启动后404/500的排查顺序我见过很多SSM新手项目启动时没有任何日志报错浏览器却抛404。这个问题的排查顺序非常重要我建议严格按下面的清单来确认项目是否成功部署到Tomcat的webapps目录。如果是用IDEA直接部署要看清楚是不是Exploded解压目录方式而不是war包方式检查访问路径。项目在Tomcat中部署时URL都会带一个contextPath。比如你的项目模块名是car_sales那访问入口就是http://localhost:8080/car_sales/login如果你访问的是http://localhost:8080/login绝对404检查web.xml和url-pattern。如果你的DispatcherServlet映射的是/那它会把所有请求都交给SpringMVC处理如果你的映射是*.do那你请求URL里就必须带.do后缀检查Spring配置文件里的component-scan路径。如果context:component-scan base-packagecom.carsales/里的包名和你的Controller实际所在包不一致Spring容器里就根本没有这个Controller自然就404了如果有404状态但Tomcat日志没有异常那多半是视图解析的问题。SpringMVC的InternalResourceViewResolver前缀配置成/WEB-INF/views/后缀.jsp但你的JSP文件路径没放对就会看到一个找不到视图的异常。这个堆栈信息是完整的属于最好排查的一类错误。至于500错误最常见的无非是空指针、数据库连接失败、没找到对应的Mapper实例这三兄弟。空指针一般出在JavaBean没有初始化就调用它的getter上数据库连接失败排查jdbc.properties里的驱动、URL、用户名密码没找到Mapper实例出厂的典型症状是启动时报org.springframework.beans.factory.BeanCreationException说找不到类型为XXX的Mapper核对mapper接口包名和MapperScannerConfigurer配置的basePackage即可。5.3 数据库连接池与中文乱码两个老演员级问题数据库连接池方面我在SSM项目里用的是Druid。Druid这个连接池除了性能好最关键的功能是监控面板。部署后访问/druid/index.html能看到实时的SQL执行记录、慢查询统计、数据库连接池状态。这个功能在项目汇报的时候展示出来非常有科技感也很容易说明自己在数据库优化方面做过功课。Druid配置里有一个注意点它的URL需要带上参数jdbc.urljdbc:mysql://localhost:3306/car_sales?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这几个参数一个都不能少尤其是characterEncodingutf8。如果不加你插入中文数据后在页面上显示出来的就是???或者乱码。如果你的MySQL版本是8.0以上还要记得把驱动类改成com.mysql.cj.jdbc.Driver并指定serverTimezoneAsia/Shanghai。很多人卡在这一步卡到崩溃。除了数据库这边Tomcat本身也可能导致乱码。我在IDEA控制台看日志的时候看到过方块字那是因为IDEA控制台的编码和Tomcat日志输出编码不一致。解决办法是在Tomcat的conf/logging.properties里把java.util.logging.ConsoleHandler.encoding改成了UTF-8同时IDAE里的文件编码也统一成UTF-8问题就消失了。6. 从课程项目到面试亮点你该打磨的细节很多同学做到项目能跑起来这一步就收工了觉得很满足。但如果你想拿这个项目去参加答辩或者写进简历我还愿意再帮你精细打磨一下让它在众多同题项目里有点不同。6.1 把系统做出产品感的4个细节技巧一设计一个命名规范的公共返回值对象。不要在你的Controller里散落着一堆返回String、返回ModelAndView、返回Map的方法。建议统一定义一个Result类里面包含code、msg、data三个字段。这样无论成功失败前端和调用方都能用一套规则来处理。技巧二全局异常处理器。SpringMVC提供了ControllerAdvice和ExceptionHandler我把它用在统一捕获业务异常和未知异常上。这个机制好好用起来你代码里的try-catch数量会大大减少异常处理逻辑也不再散落在每一个Service方法里。技巧三为每个关键业务写乐观锁或状态校验。比如订单取消操作要判断订单当前状态是否允许取消删除车辆信息时要判断该车辆是否处于在售状态或者是否已经关联过订单。这种业务规则校验是代码质量的试金石。技巧四前端标签页面的视觉统一。SSM项目通常用的是JSP即便你不擅长前端也要保证每个页面的风格是统一的导航栏、侧边栏、内容区域、按钮样式、表格样式必须一致。我的做法是把通用部分抽成一个common.jsp用% include %引入表单使用一套风格相统一的标签和CSS类。这些视觉细节虽然不是技术难点但能做到统一整齐答辩时的观感分至少能提高一个档。6.2 面试官爱问的这个项目进阶问题当你在简历上写了这个项目之后面试官大概率会顺着项目问你一些关联问题。我根据自己的经验提前整理了一份最常被问并且最能体现水平的问题清单供你自查问题期望的回答方向为什么用SSM而不是Spring Boot理解SSM底层原理更贴近Spring容器与MyBatis代理的运作过程订单创建时如何保证不同时超出库存使用事务加行锁UPDATE语句就带库存校验通过受影响行数判成功什么是Spring的Bean生命周期如何影响你的项目能围绕实例化、初始化、销毁阶段说到PostConstruct、InitializingBean等权限控制怎么实现的MyBatis如何动态拼接SQL拦截器RBACMyBatis动态SQL标签如果同时有100个人来抢购同一辆车库存1台你的系统还能正常工作吗打开行锁概念说明同一行记录上的UPDATE是串行或冲突的而不是说改成Redis这种不务实的答案Redis能解决什么问题你的项目里有必要引入吗可以说缓存数据或Session共享有意义但对于单机小体量项目没必要强行引入反而是更理性的回答第六个问题尤其容易翻车。很多同学总想在自己的项目里强行加一个Redis以显得技术栈丰富。但面试官追问用了Redis你的性能瓶颈在哪数据一致性怎么保证时往往就答不上来了。更好的策略是项目里真实的挑战是数据库事务和数据一致性Redis可以作为已知的优化方案提一句必须强调这个系统当前体量并给出不引入Redis的理由以及在什么样的情况才值得引入。有很多高分项目都在这类有主见的技术决策上胜出的。6.3 扩展方向这套系统还能往哪走如果你还有额外的精力下面3个方向可以任选一个做深能让整个项目的规格从课程设计变成准工业级方向一引入统计图表。用ECharts在管理员首页展示近6个月的销售趋势折线图、品牌销量排行柱状图。前端用Ajax请求后端接口后端返回JSON数据前端动态渲染。这个功能加的代码量不多但视觉效果极佳。方向二管理端增加操作日志。记录每个用户在系统中的增删改操作包括操作人、时间、操作内容。有了一张operation_log表之后整个项目的管理属性马上出来了。方向三数据导出功能。把销售订单列表导出成Excel文件。Java这边可以用POI不过POI的API比较啰嗦也可以引入Hutool的Excel工具类几行代码就能搞定导出。公司里那种导出并下载文件的需求跟这个是一模一样的。最后再分享一个我每次做完项目都会做的小操作在项目根目录写一个README.md把项目简介、技术栈、数据库脚本说明、部署运行方式和项目截图放进去。代码可能会被人忘记但这个README放上去之后这个项目才是完整交付的不论是自己复盘还是留给学弟学妹都是极好的总结。这个习惯我从这个汽车销售系统开始一直保留到现在算是我做项目最值得保留的经验之一。
返回列表