每年毕业设计季,我总会遇到有人问:“都什么年代了,还要用JSP做影院售票系统?会不会显得很过时?”我的回答一直很明确:这个题不仅不过时,反而是计算机毕业设计里少见的“业务富矿”。JSP + Servlet + MySQL这套组合,虽然看起来老,但它把Java Web的底层原理全暴露在你面前——请求怎么进容器、Session怎么维持、JDBC怎么连库、事务怎么控制,每一步都能亲手摸到。后来那些流行的Spring Boot框架,不过是在这套骨架上套了一层封装和自动配置而已。所以红星影城售票系统这个题,既能让导师看到完整的工程能力,又能让你在答辩时讲出真东西,非常适合作为Java方向毕业设计的首选题。
我当年带过的学生里,有人用这个题目拿了学院优秀毕设,也有人把它改改包装成了Spring Boot版本再去投简历,效果都不错。下面这篇文章,我就把这个项目从选题、需求、数据库、核心业务逻辑、代码实现到部署答辩的全过程,完整梳理一遍。无论你是打算照做复刻,还是想在这个题目上做二次开发,都值得看完。
1. 为什么红星影城售票系统值得用JSP写——选题动机与技术栈定位
1.1 业务复杂度:比“图书管理”多走了一步,又没到电商那种失控程度
很多毕业设计选题翻来覆去就是图书管理、学生管理、仓库管理,这类系统的本质是单表或两三张表的增删改查,做完之后答辩老师问你“你这个项目难点在哪”,你往往答不上来。红星影城售票系统不一样,它的复杂度刚好卡在一个很舒服的位置。
它有哪些“超纲但不越界”的点?第一是座位模型,一张票对应的不是简单的商品ID,而是影厅里的一个具体座位,座位有行列坐标,要随场次动态显示状态;第二是锁座与订单状态流转,用户选座后不能马上霸占,必须在一段时间内完成支付;第三是并发场景,同一个热门场次的同一排座位,可能同时被多个用户点中,系统必须保证不出现“一票多卖”。这三个点每一个都能在答辩时展开讲十分钟,但对一个本科生的开发能力来说又完全可控。这就是我说的“恰到好处”。
1.2 JSP没你想的那么“过时”,它是理解Java Web底层的最佳入口
现在市面上的教程几乎都在讲Spring Boot,很多同学上来就学“约定大于配置”,遇到问题都不知道框架底层做了什么。JSP项目则完全不同,它逼迫你手动写Servlet、手动配置web.xml、手动管理JDBC连接,你会清晰地感受到一次浏览器请求是怎么变成Java方法调用的。
更重要的是,这样一套系统的知识是可以迁移的。你理解了Servlet的生命周期,就能理解Spring MVC的DispatcherServlet;理解了JSP的内置对象,就能理解ModelAndView和Thymeleaf的上下文;理解了JDBC的事务手动提交,就更能明白声明式事务为什么方便。答辩老师如果问你“你这个项目换成Spring Boot要怎么改”,你完全可以从容地回答:包结构拆成Controller/Service/Mapper,数据库连接换成连接池,JSP换成Template Engine或者干脆前后端分离。这个迁移能力,恰恰是毕业设计最想考察的。
1.3 系统整体定位:前台用户、后台管理员、底层支撑三层写清楚
这个系统我建议按三层来规划,而不是简单地堆页面。
第一层是用户前台,包含注册登录、影片浏览、场次查询、选座购票、在线支付(模拟支付即可)、订单查询、退票申请这些功能。第二层是管理员后台,包含影片信息管理、影厅与座位维护、排片管理、订单管理、票房统计、用户管理。第三层是系统底层支撑,包括过滤器做登录鉴权、拦截器统一处理字符编码、JDBC工具类管理数据库连接、定时任务处理超时订单。
这里有个容易被忽略的点:管理员后台和用户前台在工程上要能明显区分,比如通过路径前缀/admin/进行统一过滤,每个后台Servlet都继承同一个BaseServlet做权限校验。这样做的好处是,答辩时你可以直接说“我有统一权限控制”,而不是“每个页面里都判断了一下是否登录”,两种说法的专业程度完全不在一个档次。
2. 需求分析先把功能边界画清楚:一张电影票的完整生命周期
2.1 用户端的六步主流程:查片、选场、选座、下单、支付、取票
拿到这个题目,第一件事不是建表,而是把一张电影票从“想看电影”到“进场”的全过程拆出来。我习惯拆成六步:
- 浏览影片列表:用户进入首页,看到正在热映和即将上映的影片,点进去能看到影片简介、海报、评分。
- 选择场次:影片详情页里展示该影片在某一天的所有排片场次,包括放映厅、开始时间、语言版本、票价。
- 挑选座位:进入选座页面,看到一个按影厅真实布局渲染出来的座位图,已售座位置灰,已锁定座位标记为“正在支付”,可选座位高亮。
- 提交订单:确认座位、场次、价格后生成订单,订单状态为“待支付”。
- 完成支付:这里做模拟支付即可,点击确认支付后订单变为“已支付”,同时座位正式标记为已售。
- 取票/退票:已支付订单可以“在线取票”(生成取票码),开场前一定时间内可以申请退票。
每一步背后都要对应一张表或者一个状态字段。我见过很多同学在设计阶段省略“订单状态”,用“订单存在就是已买”这种朴素逻辑,到做退票功能时发现表结构根本撑不住。需求分析阶段一定要把状态的演进列出来,否则后患无穷。
2.2 管理后台的四大功能面:影片、排片、订单、统计
管理员后台是体现系统完整度的地方,我建议至少规划四个功能面:
- 影片管理:对影片信息做增删改查,包括片名、导演、主演、片长、上映日期、海报图、剧情简介。注意上架下架的状态,一个影片如果没有上架,前台不展示。
- 影厅与座位管理:维护影厅名称、座位行数、列数,支持在后台生成该影厅的座位矩阵。
- 排片管理:为某个影片在某一天安排场次,需要选择影厅、开映时间、结束时间(根据片长自动算)、票价。排片时要校验同一个影厅在同一时间段不能被排两场。
- 订单管理与统计:查看所有订单列表,按用户、影片、日期筛选;可以手动关闭超时未支付订单;统计每日票房、影片票房排行、热门场次上座率。
这四个面做完,系统在功能上就是完整的,不存在“只做了一半”的感觉。很多毕设被扣分不是因为代码烂,而是因为后台管理太单薄,导师打开一看就两张表,自然觉得工作量不足。所以后台功能别偷懒。
2.3 容易被忽略的非功能需求:防重、超时、速度
功能需求之外,有几个非功能需求必须在设计文档里体现,因为答辩老师特别爱问。
第一是座位防重。两个人同时选同一个座位,系统只能放行一个。第二是锁座超时释放。用户选了座位但不支付,不能一直占着,一般设定15分钟自动释放。第三是查询性能。首页要展示影片列表和场次信息,不能一个页面触发几十条SQL慢慢等。第四是数据一致性。下订单、扣座位、减库存这三个操作要么全成功,要么全失败。
这部分不需要写得很高深,但要在设计里给出方案。哪怕方案只是“用事务+条件更新”,也说明你考虑到这些问题了。大多数同学连“还有并发这回事”都没意识,你只要提出来,就已经赢了一截。
3. 数据库设计是成败的分水岭:ER模型与核心表结构
3.1 六张核心表:职责与关键字段
数据库设计上,我最终采用的是六张核心表:用户表、影片表、影厅表、场次表、座位表、订单表。看起来表不多,但张张都有讲究。
| 表名 | 核心职责 | 关键字段 | 说明 |
|---|---|---|---|
| tb_user | 用户信息 | id, username, password, phone, role, create_time | role区分普通用户和管理员 |
| tb_film | 影片信息 | id, title, director, actors, duration, release_date, poster, status | status控制上下架 |
| tb_hall | 影厅信息 | id, name, row_count, col_count | 记录座位矩阵规模 |
| tb_session | 场次信息 | id, film_id, hall_id, start_time, end_time, price, status | 一场电影一个场次 |
| tb_seat | 座位信息 | id, hall_id, row_no, col_no, status | 座位是影厅的物理资源 |
| tb_order | 订单信息 | id, order_no, user_id, session_id, seat_id, amount, status, create_time, pay_time | 一次购票一条订单 |
每个表的主键我统一用自增id,order_no用时间戳加随机数生成唯一编号。订单表和场次、座位之间是外键关系,但注意外键约束我建议在逻辑层控制,数据库里可以不加物理外键,否则后面做班次调整时会遇到各种牵制。
3.2 座位建模的本质:座位属于影厅,状态跟着场次走
这是整个数据库设计中最容易想歪的地方。很多同学会把座位直接挂在订单表下面,或者把每场电影都复制一份座位表,结果数据量暴增且逻辑混乱。正确的做法是:
座位表只记录“这个影厅有哪些座位”,比如5号厅有6排每排10座,那么tb_seat里就有60条记录。这个表是静态的,与具体某场电影无关。座位在某一场次下是否可售,靠的是订单表和座位状态字段的配合:某个场次下,如果该座位存在一条状态为“已支付”或“待支付”的订单,那么这个座位就是占用状态。
这样做的好处是显而易见的:新增场次不需要复制座位,查询某场次可用座位时,只需要查“该场次下没有被占用的座位”,一条SQL就能列出来。同时它还让你在做退票、换座功能时不用去动座位表,只需调整订单状态,所有状态自动恢复。
3.3 订单表怎么设计才能支撑支付和退款
订单表是整个系统的账本,字段设计要一次性想到支付和退款。我常用的订单表结构包含这些字段:
CREATE TABLE tb_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, session_id INT NOT NULL, seat_id INT NOT NULL, amount DECIMAL(8,2) NOT NULL, status TINYINT NOT NULL COMMENT '0-待支付 1-已支付 2-已出票 3-已取消 4-已退款', create_time DATETIME NOT NULL, pay_time DATETIME, cancel_time DATETIME, ticket_code VARCHAR(32) );我见过有些同学把座位行列号直接写在订单表里,这样做确实查询方便,但是座位一旦调整,历史订单里的座次信息就会失真。正确的设计是订单只关联seat_id,座位信息通过JOIN去查。至于金额,用DECIMAL(8,2)而不是FLOAT,这个不用解释也要记住。
3.4 索引与外键:别让查询把服务器拖垮
毕设阶段虽然数据量不大,但索引设计是加分项。我给这些字段加了索引:
- tb_order.order_no:唯一索引,因为要按订单号查询。
- tb_order.user_id:普通索引,用于查询“我的订单”。
- tb_order.session_id:普通索引,用于查询某场次的所有订单,也是选座时判断座位占用情况的关键查询。
- tb_session.film_id:普通索引,用于查某部影片的所有场次。
- tb_seat.hall_id:普通索引,用于按影厅取座位。
查询逻辑上,选座页面的核心SQL类似这样:查出一个影厅的全部座位,然后LEFT JOIN该场次的订单表,订单状态为待支付或已支付的就认为是“占用”。用LEFT JOIN的原因是即使没有订单,也要把全部座位显示出来。
4. 锁座与状态机:售票系统区别于普通CRUD的核心
4.1 为什么必须锁座
普通商城系统是库存扣减,商品数量减一;电影院不一样,每个座位是唯一实体,不能被重复售卖。如果不做锁座,两个用户同时点击同一个座位,后端先后处理两个请求,就可能生成两条订单对应同一个座位。所以售票系统的核心,在于“锁定”这个动作。
锁座在实现上有一个简单的做法:座位状态字段加一个“锁定中”的状态。用户选座时,先把座位状态从“可用”改成“锁定”,然后创建订单。如果15分钟内未支付,再解锁。这样其他用户看到锁定状态就知道这个座位暂不可选。这个方案虽然有一个“用户A锁定后迟迟不支付会挡着别人”的体验问题,但配合超时释放机制就可以接受,而且是毕设里最容易写清楚的方案。
4.2 订单状态机的五态流转
订单状态不能只有一个字段放着,必须有一套流转规则。我给这个系统定义了五种状态,并严格控制流转方向:
| 状态码 | 含义 | 能流向的状态 | 触发方 |
|---|---|---|---|
| 0 | 待支付 | 1、3 | 用户支付/系统超时取消 |
| 1 | 已支付 | 2、4 | 用户取票/用户申请退款 |
| 2 | 已出票 | 无 | 用户在线取票 |
| 3 | 已取消 | 无 | 系统自动取消 |
| 4 | 已退款 | 无 | 管理员审核退款 |
答辩时最怕被问“如果用户支付成功了但系统没收到怎么办”,你可以回答:支付接口返回后,后端在同一个事务里更新订单状态并完成出票,不允许出现半个状态。模拟支付阶段虽然不用对接真实支付网关,但事务边界一定要写清楚,这是老师关注的核心点。
4.3 超时释放的两种实现方案
锁座超时释放,我推荐先用“惰性释放”方案,而不是一上来就做定时任务。惰性释放的思路是:每次有人查询座位时,先顺手把当前场次下创建时间超过15分钟且状态为“待支付”的订单全部置为“已取消”,然后再查询可用座位。这样不需要额外起线程,逻辑也简单。
UPDATE tb_order SET status = 3, cancel_time = NOW() WHERE session_id = ? AND status = 0 AND create_time < DATE_SUB(NOW(), INTERVAL 15 MINUTE);如果你觉得惰性释放不够“主动”,还可以在项目启动时用Java的ScheduledExecutorService每隔一分钟扫一次表,执行同样的更新。两种方案我都试过,毕设阶段选第一种就够了,第二种更适合做展示,因为它可以把“系统自动清理过期订单”做成一个日志,答辩时给老师看日志输出,比口头描述有说服力。
4.4 并发防重:一行SQL搞定一票多卖
说到“并发防重”,很多同学以为要用什么高深技术,其实核心就是条件更新。用户提交订单时,不要先查再改,而是直接执行:
UPDATE tb_seat SET status = 1 WHERE id = ? AND status = 0;这条语句的影响行数如果为0,说明座位已经被别人抢占了,后端直接提示“座位已售出”。这个操作和后续的订单插入放在同一个数据库事务里,就能保证座位状态和订单的一致性。我在代码里配合使用了数据库的SELECT ... FOR UPDATE对场次记录进行行级锁,避免同一场次多个订单同时提交时出现间隙问题。
这套方案用到的都是原生数据库事务能力,不需要引入Redis分布式锁,对于一个校园规模的项目来说完全够用。答辩时你如果能说清楚“为什么Condition Update可以防止超卖”,老师对你的印象会非常深刻。
5. 从DAO到JSP页面:核心模块的代码级实现思路
5.1 MVC包结构:让答辩老师一眼看懂你的工程
打开一个JSP毕设工程,最怕看到包名乱成一团。我推荐按下面这套结构组织工程,既符合MVC思想,又方便答辩讲解:
com.redstar.cinema ├── entity # 实体类:Film, Hall, Session, Seat, Order, User ├── dao # 数据访问层:FilmDao, SeatDao, OrderDao ├── service # 业务层:CinemaService(锁座、下单、退票) ├── servlet # 控制层:FilmServlet, OrderServlet, AdminServlet ├── filter # 过滤器:EncodingFilter, LoginFilter ├── util # 工具类:DBUtil, DateUtil └── listener # 监听器:ContextListener(启动时初始化)控制层里的Servlet按业务域分组,不要一个功能写一个Servlet。我习惯把用户相关的操作合并到UserServlet,通过method参数区分login、register、logout等动作;订单相关合并到OrderServlet,区分submit、pay、cancel、list。这样类少而职责清晰,导师检查代码时不会觉得凌乱。
5.2 选座页面的数据结构:座位矩阵怎么渲染
选座页面的核心数据结构是一个List<Seat>,每个Seat包含行号、列号、状态。服务端在一次查询中,把某影厅的全部座位取出来,再结合当前场次的订单占用情况,给每个座位标一个“available/locked/sold”的状态,然后放进request域。
JSP页面渲染时,我建议先按行分组,再用嵌套循环输出表格。这里可以用一个Map按行号聚合:
Map<Integer, List<Seat>> seatMap = new TreeMap<>(); for (Seat seat : seatList) { seatMap.computeIfAbsent(seat.getRowNo(), k -> new ArrayList<>()).add(seat); } request.setAttribute("seatMap", seatMap);然后在JSP里用JSTL双层遍历,行是<c:forEach>的一层,列是第二层。每个座位渲染成一个可点击的div或td,可用座位是浅色,锁定座位是橙色,已售座位是灰色且不可点击。选定座位后,用一个隐藏的input存储seatId,提交订单时把这个值传给后端。这块写完,整个项目最直观的页面效果就出来了。
5.3 提交订单的Servlet事务组合拳
下单接口是整个系统最重要的方法,我贴一段核心逻辑供参考:
public boolean createOrder(int userId, int sessionId, int seatId) { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 条件更新锁座 SeatDao seatDao = new SeatDao(); int rows = seatDao.lockSeat(conn, seatId); if (rows == 0) return false; // 2. 查询场次信息,计算票价 Session session = sessionDao.findById(conn, sessionId); // 3. 生成订单号并插入订单 String orderNo = generateOrderNo(); int insertRows = orderDao.insert(conn, orderNo, userId, sessionId, seatId, session.getPrice()); if (insertRows == 0) { conn.rollback(); return false; } conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw new RuntimeException("下单失败", e); } finally { conn.setAutoCommit(true); DBUtil.close(conn); } }这里的关键点是:lockSeat必须和insert在同一个事务里。如果先锁座后插单,中间抛异常一定要回滚,否则会出现“座位锁了但订单没有”——这个坑我见过太多人踩。为了减少代码重复,我在DAO层把每个方法都加了Connection参数,让Service层统一控制事务,而不是让DAO自己拿连接自己提交,这一点是要特别注意的。
5.4 EL与JSTL:JSP页面里别写Java脚本
JSP页面里密密麻麻写着<% %>脚本一定会被老师扣分。正确的做法是:Servlet只负责把数据放进request域,JSP页面只用EL表达式和JSTL标签渲染。
比如影片列表渲染:
<c:forEach items="${filmList}" var="film"> <div class="film-card"> <h3>${film.title}</h3> <p>类型:${film.type} | 片长:${film.duration}分钟</p> <a href="${pageContext.request.contextPath}/film?action=detail&id=${film.id}">查看场次</a> </div> </c:forEach>注意${film.title}实际上是通过getter方法取值的,所以实体类必须写标准的getter/setter。这是我见过很多新手门禁前摔倒的地方——实体类的属性名和数据库字段名对不上,或者忘了写getter,导致页面上一片空白。开发JSP时,只要数据没显示,第一反应去检查实体类的getter、变量名拼写、setAttribute的key是否一致。
5.5 前后台分层的三点建议
最后补三个分层建议。第一,所有Servlet都继承一个BaseServlet,BaseServlet里封装了获取模板、渲染错误页、读取当前登录用户的方法,减少重复代码。第二,Admin相关的Servlet统一加上管理员的权限校验,普通用户访问后台接口直接跳回首页。第三,Service层不要直接暴露给Servlet,Servlet只调用Service接口,这样以后就算把JSP换掉,业务逻辑还能复用。这些细节在你写代码的时候未必有直观感觉,但等到答辩总结项目亮点时,全是能说出来加分的东西。
6. 开发与部署避坑清单:我踩过的那些真实问题
6.1 数据库连接池:两分钟报ClassNotFoundException?
很多同学在地址栏一启动Tomcat就报错,然后开始怀疑人生。我遇到过最多的原因是JDBC驱动包没放进WEB-INF/lib目录,或者数据库连接串写错。
推荐直接使用C3P0或Druid连接池,不要自己写DriverManager.getConnection。自己管理连接会有两个问题:一是频繁开关连接导致数据库压力大,二是代码被老师看到后容易被问“你的连接泄漏怎么处理”。用连接池的话,这些坑都可以绕开。
Druid的配置示例:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/cinema?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" /> <property name="username" value="root" /> <property name="password" value="123456" /> </bean>6.2 中文乱码的三个根源
中文乱码是JSP项目的“老朋友”。乱码出现,先按这三步查:
第一,JSP文件顶部的pageEncoding要设为UTF-8,同时编辑器右下角的文件编码也要是UTF-8,否则文件本身存的就是乱码。第二,POST请求的参数要统一设置编码,我建议写一个EncodingFilter,在过滤器里对request和response都设置UTF-8。第三,数据库连接串上必须加characterEncoding=utf8,而且要注意MySQL 5.7和8.0对时区参数的要求不同,MySQL 8.0需要加serverTimezone=Asia/Shanghai,否则能连上但取时间会报错。
只要这三处统一,乱码基本可以杜绝。以后排查乱码问题,别东改一下西改一下,就按这个链条从上到下捋。
6.3 Tomcat部署:war包名与上下文路径
开发时用IDEA内置Tomcat跑起来很容易,但要提交的时候,还是得导出war包部署到独立Tomcat里。这一步有几个细节:
- 导出的war包名默认就是访问路径,如果想访问
http://localhost:8080/,就把war包改名为ROOT.war再扔进webapps目录。 - 再次部署前,先停止Tomcat并删除旧项目目录,否则会残留缓存类。
- 端口被占用时,打开
conf/server.xml改<Connector port="8080">端口号。
我把部署流程写成一个小脚本,每次打包自动清理旧目录、拷贝war、重启服务,省了非常多事。
6.4 联调与调试手段
JSP项目的调试比前后端分离的项目要麻烦一些,因为页面渲染出了问题往往表现为白屏或者500。我的经验是:第一,查看Tomcat的logs/catalina.out日志,按时间定位第一条Exception;第二,在Servlet里临时打印关键参数,或者用System.out输出到控制台;第三,用浏览器开发者工具看网络请求,确认请求是否到达Servlet、返回状态码是多少;第四,把SQL语句打印出来,在Navicat里手动执行一遍,能快速判断是SQL写错还是数据不对。
这里特别建议在DBUtil里加一个开关,开发阶段打印所有SQL日志,上线前再关掉。每天写代码前先瞄一眼数据库连接是否正常,能省下大量排查时间。
7. 答辩演示准备:把做过的系统讲成一套完整业务
7.1 演示黄金路线:提前准备一个完整业务故事
答辩现场最忌讳的是上来就点“登录、进去、点几个按钮、完事”。我建议你提前准备一条完整的业务演示路线,把它讲成一个故事:
第一步,用管理员账号登录后台,添加一部新影片,配置一个影厅,排一个今天下午的场次。第二步,切换到前台,注册一个新用户,浏览影片,点进这个刚排好的场次,看到座位图。第三步,故意选一个中间排靠中间的座位,提交订单,演示待支付状态。第四步,用另一个用户登录,进入同一场次,展示刚才那个座位已经显示“锁定中”,这就是锁座的直观表现。第五步,回到第一个用户支付,刷订单页面看到状态变成已支付。第六步,打开后台订单管理,看到订单信息,点击统计报表,展示今日票房和上座率。
这条路线走完,系统的所有核心功能都被串起来了,而且自然地把“锁座”“状态流转”“后台管理”都讲到了。比零散点按钮强太多了。
7.2 三个必背问题的答题框架
答辩老师翻来覆去问的问题其实就那几个,提前准备好框架,底气完全不同。
第一个问题:“为什么用JSP不用Spring Boot?”答题框架是:JSP/Servlet是Java Web的基础,能更清楚展示HTTP请求处理、Session管理、JDBC交互等底层机制,符合本科阶段对“知其所以然”的要求;同时项目采用MVC分层,可以平滑迁移到Spring Boot。
第二个问题:“数据库为什么这么设计?”答题框架是:座位是影厅的物理资源,订单与座位关联而不是拷贝座位数据,保证数据一致性;订单状态字段支撑完整的生命周期;索引设计针对高频查询。
第三个问题:“两个用户同时买同一个座位怎么办?”答题框架是:使用事务加条件更新,UPDATE ... WHERE status=0,影响行数为0时说明已经被占用;同时在事务内锁定场次行,保证并发取座不会出现超卖。
这三个框架你背熟之后,不管老师从哪个角度追问,你都能接得住。
7.3 最后一点个人体会
做完这套系统,我对“毕业设计的本质”有了更清楚的认识:它不是让你做一个多么前沿的工程,而是考察你能不能把一门技术栈老老实实落到一个具体业务里。红星影城售票系统让我在锁座、事务、状态机这些问题上真正想通了原理。如果你也选了类似题目,我希望你别急着去抄代码,而是先把这个系统的业务逻辑和表结构画在纸上,你在草稿纸上想清楚的那一天,整个项目其实已经完成了一半。后面敲代码,只是把你想清楚的方案翻译给计算机听罢了。