ARTICLE DETAIL

资讯详情

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

从SSM到实战:二手交易平台的核心设计与并发安全

从SSM到实战:二手交易平台的核心设计与并发安全 1. 这个项目到底在解决什么问题需求拆解比写代码更重要做二手物品交易网站听起来是个老掉牙的课题——凡是学Java Web的人十有八九都做过或者见过类似的东西。但我盯上这个项目恰恰是因为它“老”老到很多人根本不重视它背后的逻辑上来就建表、写Controller、套个前端模板最后做出来的东西看起来五脏俱全实际上一推敲就露馅。先把这个系统的核心需求看清楚。二手交易和普通商城最大的区别在于商品的发布者是C端用户不是运营人员。这就带来三个连锁问题。第一商品数据是高度异构的。一个卖二手手机的人想填“电池健康度”一个卖二手书的人想填“几成新”你不可能像京东自营那样为每个品类设计一套标准属性。所以数据模型不能一上来就按“商品固定字段”设计得考虑动态属性怎么存、怎么查。第二交易信任成本高。买卖双方都是普通用户平台要管的不只是“商品有没有上架”还有交易状态怎么流转——从发布、被下单、买家确认、到订单完成每一步都可能出现“拍了不付款”“发货不确认”的情况。状态机设计是这个系统的隐藏核心。第三搜索和筛选的逻辑比想象中复杂。二手商品的标题是用户自己写的乱七八糟什么风格都有你不能指望数据库LIKE一把梭就能让用户满意。价格区间、成色筛选、地域偏好这些条件组合起来怎么走索引是个很现实的性能问题。我见过太多同类项目把精力花在了“界面好看”上用了各种花里胡哨的Bootstrap模板、Vue组件结果连最基本的“同一件商品被两个人同时下单”这种并发问题都没处理。这篇文章我打算把SSM这套经典组合怎么落地这些需求讲清楚重点不在于给你贴几百行代码而在于把“为什么这么设计”说明白你能拿去应对课程设计、毕业设计也能在面试时把项目讲出层次。2. SSM选型不是“老古董”这套组合在今天依然有它的合理位置我在项目里选择SSM不是因为Spring Boot不好而是因为在这个场景下SSM反而能把“框架是怎么工作的”展示得更透明。很多初学者用Spring Boot写了一年代码可能都没搞清楚DispatcherServlet到底做了什么因为内嵌Tomcat和自动配置把一切都抹平了。SSM三个组件各自负责的事情分得很清楚Spring管对象。Service层的类、DAO层的Mapper这些Bean的生命周期、依赖注入都是Spring容器在管。这也是整个系统能解耦的根基。SpringMVC管HTTP。请求从Tomcat进来DispatcherServlet负责找合适的Controller方法把参数绑定好返回视图或JSON数据。这个过程你可以在XML里看到每一个配置项。MyBatis管SQL。Mapper接口只定义方法签名真正的SQL写在XML文件里动态SQL能根据条件拼出不同的查询语句非常适合二手商品这种“条件组合多变”的场景。你可能会说“这些Spring Boot不也都有吗”对都有但Spring Boot把约定大于配置做到了极致你只需要引入一个starter一切都被自动配置好了。而SSM需要你手写web.xml、applicationContext.xml、spring-mvc.xml、mybatis-config.xml这个写一遍的过程就是把HTTP请求生命周期、Spring容器初始化顺序、MyBatis会话工厂绑定过程全部过一遍的过程。举个例子SpringMVC的DispatcherServlet和Spring的ContextLoaderListener这两个东西在Spring Boot里你根本不需要碰它们但在SSM里你必须理解ContextLoaderListener先初始化根容器加载Service、Mapper这些业务BeanDispatcherServlet再初始化自己的子容器加载Controller。子容器能看到父容器的Bean反过来不行。这个机制直接决定了你的事务注解应该放在Service层而不是Controller层——因为事务管理器通常在根容器里Controller在子容器中如果事务加在Controller上增强逻辑可能压根没被事务管理器处理。还有一点是MyBatis和Spring的整合方式。很多人用MyBatis的时候习惯于直接用SqlSessionFactoryBuilder去构建会话但在SSM里你需要借助MyBatis-Spring这个桥接包让SqlSessionFactory交给Spring容器管理并且通过MapperFactoryBean把Mapper接口动态代理成Bean。这个过程中Mapper接口没有实现类只有XML里的SQLSpring是怎么把它变成可注入的对象的靠的就是JDK动态代理——启动时扫描Mapper接口为每个接口生成代理对象方法调用时根据全限定名去找XML里的statement。理解了这一层以后你看到“Mapper标了Autowired但报NoSuchBeanDefinitionException”这类问题才不至于两眼一抹黑。3. 数据模型设计二手商品表的坑与解法数据模型是整个系统的地基这段我踩过很多次坑值得单独拿出来说。3.1 商品主信息表不可变字段抽出来先从最核心的product表说起。我当时的设计是把“所有商品都有的信息”放进主表主要包括product_id主键user_id发布者IDtitle标题用户自定义varchar长度给到120description描述text类型pricedecimal(10,2)精确到分original_price可选decimal(10,2)category_id分类IDstatus上架状态/交易状态tinyintview_count浏览次数int默认0created_timeupdated_timeprovincecity区域ID用于地区筛选这个表设计本身不难难的是字段取舍。比如original_price很多二手平台的表单里是选填项那数据库里就允许NULL查询的时候要用IFNULL做兜底。再比如status字段我想特别强调一下——不少人喜欢给状态存字符串比如“on_sale”“sold_out”“pending”这非常坑。字符串的比对效率低数据校验不严格而且状态流转写起来很容易出错。更好的做法是用tinyint存数字枚举比如0表示已下架、1表示在售、2表示已被下单锁定、3表示交易完成。代码里写常量类或者枚举去对应数据库里只存数字。为什么要有“已被下单锁定”这个状态这是二手交易特有的问题。平台必须保证同一件商品同一时间只被一个买家下单。如果不加锁定状态A买家下单后还没付款B买家又下单了最后就乱套。处理方式是在买家提交订单接口里用一个条件更新SQL——UPDATE product SET status 2 WHERE product_id ? AND status 1。受影响行数为1说明抢锁成功为0说明商品已经不是可售状态。这个通过“乐观锁”思路完成状态流转的方案比先SELECT再UPDATE的方式安全得多。3.2 动态属性扩展表比JSON字段在SSM中更顺手商品动态属性怎么存同类项目里有拍脑袋用JSON字段的——直接在product表加一个attributes字段把各种属性塞进一个JSON字符串里。但SSM项目有个特殊性MyBatis对JSON的支持远没有JPA那么便捷你要么用TypeHandler自己去解析要么在Service层手写序列化和反序列化。这其实还能接受真正的问题是查询——如果有人要筛选“电池健康度大于90%的手机”JSON存储会让SQL写到你怀疑人生。所以我更推荐“主表扩展属性表”的设计思路。逻辑是这样的凡是可能被筛选的属性比如成色、品牌、型号、保修情况都拆出来做成键值对的形式存一张product_attribute表id主键product_id商品IDattr_name属性名attr_value属性值这样有一个好处属性可以随时扩展今天来了个卖摩托车的用户要填“行驶里程”你不需要改表结构直接存一行数据就行。查询的时候用GROUP BY product_id配合HAVING COUNT(DISTINCT attr_name)的方式来过滤逻辑也不复杂。同时也别把所有东西都往这张表里塞。像“手机品牌”这种使用频率很高的筛选维度要么在product主表冗余一个字段要么建单独的索引。实际上二手交易网站的搜索热点非常集中品牌、价格区间、地域这三板斧占了绝大多数筛选需求把高频属性做主表的正式字段把低频自定义属性放扩展表是性能和灵活性的最佳平衡点。3.3 订单表和交易状态机订单表的核心字段是order_id、order_no业务订单号要唯一且不暴露自增ID、product_id、seller_id、buyer_id、amount订单金额注意下单时就把价格快照存下来不能去关联商品表的实时价格否则卖家中途改价会导致纠纷、status、created_time、pay_time、confirm_time。订单状态流转是整个系统里最容易出逻辑漏洞的地方。我设计的状态包括0待付款1已付款待发货或线下交付待确认2交易完成3已取消每一步转换都必须有对应的条件校验。比如只有状态为0的订单才能取消只有状态为1且买家确认收货或者卖家确认收款后才能变成2。这些状态流转不能散落在Service的各个方法里靠开发者自己记最好用一组状态机的常量定义甚至写一个简单的状态机工具类集中管理合法的迁移路径。还有一个细节这里的订单和普通电商订单不一样二手交易大多数是线下见面交易或者第三方平台转账平台本身不涉及支付渠道对接。所以“支付”这个动作很多时候只是一个标记——买家点“确认付款”系统把订单状态从0改成1同时把商品状态锁定。理解了这一点你会发现这个系统的核心不是支付功能而是信用标记和流程引导。这也是它在业务上清晰的边界做一个交易信息撮合平台而不是做金融系统。4. 后端分层设计从Mapper到Controller每一层到底该写什么4.1 Mapper层MyBatis动态SQL处理条件查询二手商品列表页是一个典型的多条件筛选场景用户可能选分类、填价格区间、按成色过滤、按地区筛、按时间排序。SQL查询条件不确定用MyBatis的where标签配合if标签拼动态SQL是标准解法。select idselectByPage resultTypemap parameterTypemap SELECT p.*, u.nickname as seller_name, u.avatar FROM product p LEFT JOIN user u ON p.user_id u.user_id where p.status 1 if testcategoryId ! null AND p.category_id #{categoryId} /if if testminPrice ! null AND p.price gt; #{minPrice} /if if testmaxPrice ! null AND p.price lt; #{maxPrice} /if if testcityId ! null AND p.city #{cityId} /if if testkeyword ! null and keyword ! AND (p.title LIKE CONCAT(%, #{keyword}, %) OR p.description LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY p.created_time DESC LIMIT #{offset}, #{pageSize} /select这里有个很直观的性能问题LIKE查询走不了索引所以在数据量大之后这个方案会撑不住。但对于一个课程设计或中小规模的二手交易平台数据量在万级到十万级左右全表扫描加优化过的MySQL查询完全能扛住。如果真想优化方向是全文索引或ES但那就超出SSM单体应用的范畴了。很多人的MyBatis动态SQL写出来能跑但细节经不起推敲。比如参数判断用! null还是! 要分清价格筛选的边界值用gt;还是gt;导致带上临界值的问题parameterTypemap时注意键名大小写以及分页时offset和pageSize的计算方式——页码从1开始的话offset等于(pageNum-1)*pageSize。4.2 Service层事务边界与业务流程编排Service层是这个系统的灵魂所有业务规则都应该在这一层落地。让我用发布商品这个最常见的操作来说明。发布商品的流程包括写入product主表、写入product_attribute扩展表、给用户发布数加1。这三步必须在一个事务里——主表写进去了扩展表写失败了就会产生一个残缺的商品信息。所以Transactional注解加在Service方法上而且要注意默认情况下这个方法必须是public的并且通过Spring代理调用才有效。另一个重点是“发布者不能同时是购买者”。你想象一个场景用户A发布了一个商品用户B来下单订单成交通知里要同时带上双方的信息。如果某个用户发布商品后自己又去下单自己的商品这在业务上是应该被禁止的。校验逻辑很简单就是对比Product中的user_id和当前登录用户的ID查出来不一样就抛异常。Service层还有一个容易被忽略的职责把Controller传来的DTO转换成数据库实体或者反过来。比如前端传来一个JSON格式的List里面是动态属性键值对Service要负责遍历这些属性逐个Set到ListProductAttribute里再批量插入。批量插入用MyBatis的foreach循环一次insert里插入多条别在for循环里调用单条insert那是最典型的性能杀手。4.3 Controller层参数校验与登录拦截Controller层的核心原则是薄越薄越好。它只干三件事取出Session或Token里的当前用户、把请求参数绑定成对象、调用Service拿到结果后返回统一格式的JSON。对于“浏览商品”这类接口用户即使没登录也应该能访问但对于“下单”“收藏”“发布商品”这类接口必须要求登录。这个逻辑用SpringMVC的拦截器来实现最合适。mvc:interceptors mvc:interceptor mvc:mapping path/order/**/ mvc:mapping path/product/publish/ mvc:mapping path/favorite/**/ mvc:exclude-mapping path/order/list/ bean classcom.example.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptorsLoginInterceptor里做的事情非常简单从request的session里取user对象如果不存在就返回一个状态码为401的JSON。这里有个很重要的细节——拦截器不处理静态资源。你的商品图片如果放在项目内的static目录下且图片路径是以/productImg/开头的那一定要在拦截器配置里把静态资源路径排除掉否则你会看到浏览器里图片资源全部403排查半天才发现是拦截器把图片请求也拦了。Controller层的参数校验我建议不要用JSR-303注解那一套在SSM老项目里加validation依赖容易和旧版本Tomcat冲突。自己写一个简单的校验工具类或者在Service入口处做if判断抛自定义异常效果一样代码还更直观。比如发布商品的请求里标题为空、价格小于等于0、description过长这些在Service入口直接判断并抛出BusinessException会被全局异常处理器捕获并返回给前端。5. 从“能用”到“好用”搜索、排序、图片处理的实用细节5.1 搜索与排序的策略选择我在前面提到LIKE查询的局限性但作为单体SSM项目这个方案是性价比最高的。如果你想让搜索结果稍微“智能”一点可以引入一个简单的分词方案比如把用户输入的关键词按空格切分每个词都作为一个LIKE条件拼接起来String[] words keyword.trim().split(\\s); for (String word : words) { // 每个词都拼一个title LIKE条件 }这样用户搜“iPhone 12 256G”的时候会把包含任意一个词的商品都搜出来比整句匹配效果好很多。排序方向上除了默认的发布时间倒序二手交易平台通常会提供“价格从低到高”“价格从高到低”的排序以及“最新发布”排序。这个可以在查询参数里加一个sortField和sortOrder动态拼接ORDER BY子句。这里要防一手SQL注入——不要直接拼接用户参数排序字段名先映射成白名单比如sortField等于price时实际拼“p.price”等于time时拼“p.created_time”方向只允许asc和desc两个字面量。5.2 商品图片上传真实落地的方案二手商品对图片的依赖非常高一个商品至少得有三张图。图片上传功能的实现要考虑三个问题存哪里、怎么存、怎么访问。首先图片不能直接存数据库的BLOB字段除非你不在乎数据库膨胀到几个G。常规方案是存磁盘路径或云存储。对SSM项目来说校内部署或小项目部署把图片存在服务器的一个独立目录比如/usr/local/upload/数据库存相对路径最合适。SpringMVC里处理上传需要配置CommonsMultipartResolver注意两个参数maxUploadSize控制单次上传总大小defaultEncoding设置UTF-8防止文件名乱码。接收文件的Controller方法参数用MultipartFile调用transferTo方法直接落盘。PostMapping(/product/uploadImage) ResponseBody public Result uploadImage(RequestParam(file) MultipartFile file, HttpServletRequest request) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); // 白名单校验扩展名 if (!Arrays.asList(.jpg, .jpeg, .png, .gif, .webp).contains(ext.toLowerCase())) { return Result.error(不支持的图片格式); } String newFileName UUID.randomUUID().toString().replace(-, ) ext; String filePath uploadDir / newFileName; try { file.transferTo(new File(filePath)); String visitPath /upload/ newFileName; return Result.success(visitPath); } catch (IOException e) { return Result.error(图片上传失败); } }扩展名白名单校验是必须的不然用户上传一个.jsp文件放到你的静态目录里配合某些服务器配置不当的情况就是一个非常严重的漏洞。真实项目里还应该做内容校验比如读取文件头判断是不是图片魔数而不只是信任扩展名。这里我强烈建议至少做到扩展名白名单加UUID重命名这一步能防住90%的风险。图片访问路径怎么映射如果你用的是Tomcat最简单的方式是配置虚拟目录在server.xml的Host节点里加一句Context映射把磁盘路径映射到/upload这个URL前缀。不推荐把图片放到项目内的WebRoot下因为项目一旦重新部署或clean图片全没了——这个坑我踩过一次血的教训。部署在本地测试环境还可以凑合但凡是正式部署必须把上传目录和项目目录分离。5.3 收藏功能的业务细节收藏是二手交易网站的高频交互功能看起来简单实际还是有设计空间的。核心数据表favorite只需要用户ID、商品ID、创建时间三个字段唯一索引建在(user_id, product_id)上防止重复收藏。但有几个问题值得注意。一是收藏接口的幂等性——用户连点两次收藏按钮应该只产生一条记录。实现方式可以是在Service里先查再插也可以捕获DuplicateKeyException后直接返回成功。二是“收藏列表里的商品下架了怎么办”界面上应该显示“已失效”状态这是通过查询时LEFT JOIN product表并判断product是否为空实现的。三是收藏数要不要在商品表加冗余字段我的建议是加一个favorite_count发布商品列表和商品详情页都显示这个数字每次收藏或取消收藏时做增减。这样虽然多了一步数据一致性维护但换来了列表页不需要COUNT查询的性能优势值。6. 并发与安全这套系统最容易被看轻的两个维度6.1 超卖问题的本质与解决虽然二手交易不是高并发场景但“同一件商品被两个买家同时下单”的问题是真实存在的。我在前面提到了状态字段加条件更新的方案这里把完整流程展开说一遍。场景是这样的买家A和买家B同时看中一个商品同时点击“立即购买”。如果代码逻辑是先SELECT再INSERT两个请求都查到了status1然后都插入订单就产生了重复订单。解决方式把状态检查从“应用层校验”下沉到“数据库层面的原子操作”UPDATE product SET status 2 WHERE product_id #{productId} AND status 1执行这条SQL返回受影响行数。是1说明抢购锁定成功继续创建订单是0说明商品已经被别人抢走直接抛“商品已下架或已被购买”异常。在Service方法上加Transactional把UPDATE和INSERT订单包进同一个事务就能保证锁定商品和创建订单要么都成功要么都回滚。这里的核心思想是用数据库行锁替代应用层的同步锁既简单又可靠。6.2 用户输入是一等安全威胁来来来我把安全问题当成一个重要话题。二手交易系统面向的是全网用户用户输入就是最大的攻击面。至少要做如下几个事情第一XSS防御。用户在商品标题、描述、留言里输入的script标签如果原样输出到页面上就可能被执行。防御措施是后台对用户输入做HTML转义或者前端框架统一做了转义。如果是JSP时代的老项目可以使用JSTL的c:out标签输出它会自动转义特殊字符。如果是前后端分离用JSON返回数据Vue或React默认也会转义问题不大但要防止用户通过接口直接提交带HTML的字符串所以在Controller入口统一过滤是个好习惯。第二SQL注入防御。MyBatis的#{}方式默认是预编译已经有效防住了大部分注入。主要的风险点在于ORDER BY这样的位置不能用#{}只能拼字符串这就回到了我之前说的“排序字段白名单”思路。还有LIKE查询里要小心拼接%用CONCAT处理可以保证参数化。第三越权访问。这是个容易被忽略的问题。用户A登录后如果通过改URL的ID去查看、修改、删除不属于自己的数据就属于越权。处理方式是在Service层对当前登录用户与数据归属进行校验比如订单只有买家或卖家本人能查看商品只有发布者本人能编辑。这个校验在Controller和Service都要做Controller做拦截是为了用户体验Service做校验才是真正保护数据安全。6.3 前端接口的幂等处理用户连续点击两次下单按钮实际上会发送两个相同的请求。即使后端有商品状态锁也要考虑接口层的幂等设计。简单做法是下单按钮点击后立即置灰防重复点击但这只能防君子不能防小人。更稳妥的方式是前端生成一个requestIdUUID下单接口接收这个参数并且把(userId, requestId)加上唯一约束重复请求插入失败后捕获异常返回“订单处理中请勿重复提交”。这种处理方式在真实项目里很常见面试聊到并发和幂等的时候也是一个很好的加分点。7. 实测中最容易翻车的几个细节SSM配置与部署的避坑记录7.1 applicationContext.xml和spring-mvc.xml到底各管什么我在项目刚开始搭建框架时经常看到有人把所有的Bean都一股脑扫描进spring-mvc.xml里结果运行时报各种诡异错误。其实这两个配置文件的职责边界非常固定applicationContext.xml根容器扫描Service、Repository、Component配置数据源、事务管理器、MyBatis的SqlSessionFactory、Mapper接口扫描。这里面的Bean是全局共享的。spring-mvc.xml子容器只扫描Controller配置注解驱动、视图解析器、静态资源映射、拦截器、文件上传解析器。如果不小心在spring-mvc.xml里也扫描了Service就会导致容器出现同一个Bean的实例——一个在根容器里一个在子容器里事务增强的配置会出现混乱Service方法上Transactional时好时坏DAO事务管理失效但你不一定能第一时间看出来。这类问题我遇到过太多次了建议强制执行“两个配置文件扫描策略边界”根容器配context:component-scan base-packagecom.example.service/这种精确路径spring-mvc.xml里只配context:component-scan base-packagecom.example.controller/。7.2 MyBatis的Mapper扫描配置如果SSM整合时遇到Injection of autowired dependencies failed的问题大概率是Mapper没有被Spring容器管理。目前最稳妥的做法是在applicationContext.xml中配置MapperScannerConfigurerbean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.dao/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean它会对basePackage下所有接口生成代理并注册到Spring容器里。注意属性是sqlSessionFactoryBeanName而不是sqlSessionFactory这是因为MapperScannerConfigurer这个Bean的初始化时机很早如果用ref属性引用SqlSessionFactory的Bean实例可能会导致提前初始化依赖的Bean出现循环依赖问题。用字符串名字延迟查找能规避这个坑。7.3 懒加载与JSON序列化的相爱相杀如果你在商品详情页要展示发布者信息数据库设计是product表关联user表的seller_idMyBatis的resultMap里配置了关联查询并且开启了懒加载。那么当Controller把Product对象转成JSON返回给前端时序列化器访问product.getUser()会触发懒加载查询。这本身没问题但如果懒加载配置或依赖不对比如mybatis-config.xml里少了setting namelazyLoadingEnabled valuetrue/或者CGLIB代理不可用就会出现序列化时抛LazyInitializationException。一个更稳妥的方案是如果详情页需要展示的信息本来就固定直接使用MyBatis的JOIN查询一次性查出所有字段返回一个Map或自定义VO不用resultMap的关联映射。少一层懒加载就少一类问题。7.4 部署环境下的数据库初始化最后提一个很实际的部署问题。很多人在本地开发用MySQL 5.7部署到服务器上的MySQL 8.0结果项目跑起来各种字符集乱码、排序规则不正确、SQL语法不兼容。建议数据库连接串里从一开始就加上几个核心参数useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。前两个保证连接层字符集一致第三个避免MySQL 8.0默认SSL的手续费第四个是关键MySQL 8.0对时间类型的处理发生了变化不指定时区会导致日期字段读写错乱。SQL文件写完之后也建议统一导出和执行。数据库schema建表语句里统一使用utf8mb4字符集和utf8mb4_general_ci排序规则给所有需要模糊搜索的字段建索引前先想清楚这个表的写入频率别在大字段上硬建索引。8. 写在最后SSM项目做下来最值钱的东西我反反复复做了好几轮这类二手交易系统个人体会最深的一点是真正把这个项目吃透的人收获的绝不只是几个配置文件怎么写而是理解了“一个完整应用从用户请求到数据库落盘”的整条链路。JSP或Thymeleaf页面发出请求DispatcherServlet匹配Controller方法Controller调用ServiceService开启事务并操作MapperMapper执行预编译SQL数据库返回结果结果再一层层封装回JSON响应给前端——在这个闭环里每一层都有自己的职责任何一层越权都会引发连锁问题。这种分层意识是进入任何Java后端项目最基本的素养。如果还往前走一步我可以告诉你哪些地方是下一个阶段的提升方向比如引入Redis做商品列表页缓存把热门商品的查询从数据库里解放出来比如把文件上传改成OSS云存储解决单机磁盘扩展问题比如引入MQ做订单超时自动取消避免用户拍下不付款导致商品一直锁定。但这些都是“从能用走向可用”的优化不是“从不会走向会”的跨越。在动手写代码之前先把商品的交易状态流转、订单的状态机、以及并发下如何锁定商品这三件事想清楚。这三件事想清楚了项目的地基就稳了剩下的细节都是按部就班的填充。反而是那些一上来就急着把注册登录页面做出来的人十个有九个后期都在改表结构改到怀疑人生。最后分享一个我自己的习惯每写一个模块之前先用一两段话把“这个模块要处理哪些输入、哪些输出、有哪些异常分支、状态是怎么流转的”写出来然后再动手。这样看起来是慢了但整体下来无论是调试时间还是返工次数都大幅减少。做工程慢就是快这四个字在SSM这个“老家伙”身上体现得格外充分。
返回列表