简介:面向Java Web学习者和高校毕设学生的超市积分管理系统完整项目资料包,以会员积分、商品管理等典型业务场景为线索,帮助读者掌握从需求分析到编码实现的全流程。压缩包仅18.21MB,共5个文件,其中sql为数据库脚本、doc为项目报告、两个zip分别存放源代码与项目截图,另有txt说明文件,结构清晰、即下即用。内容覆盖MVC设计模式、Servlet/JSP交互、JDBC数据库访问及DAO层封装等关键知识点,适合作为课程设计或毕业设计的参考范本。资源提供完整源码、数据库建表脚本、项目报告与答辩PPT,报告详述了需求分析、系统设计与测试过程,PPT则提炼技术选型与功能模块,便于梳理答辩思路。目前已有360人学习下载,可在短期内快速搭建并二次开发,尤其适合用来理解Java Web经典分层架构与数据库操作实践。
1. 先别急着解压源码:超市积分管理系统到底在解决什么问题
很多同学拿到「基于Java的超市积分管理系统设计与实现」这个标题,第一反应是解压源代码跑起来。我劝你先停手——这个题目真正的难点不在代码,在数据库和规则设计。它要解决的问题很具体:顾客办会员卡,收银结账按规则累积积分,会员拿积分兑换商品,后台还要管积分过期和流水对账。说白了就是一个小型积分账本,表面是CRUD,内里是事务、锁和数据一致性。对于在做Java课程设计、数据库课程设计,或刚学完Spring Boot/MyBatis想练手的人,正好把「设计→实现→避坑→答辩」完整串一遍。下面直接按我做课设的完整路线讲,读完后你能少踩一半的坑。
2. 数据库设计先行:先把积分账本的五张表落下来
代码可以慢慢迭代,但表结构错了后面全要返工。超市积分系统的数据流本身很清晰:消费进来一笔钱,按规则变成积分;积分出去,变成一件商品。只要把「进来」和「出去」两条链路用表记录下来,功能就完成了一半。所以我拿到项目报告的第一件事不是写代码,而是把ER图和数据字典画清楚。这一步做扎实了,后面的源代码写起来就是照着表搬数据。
2.1 角色与核心流程:会员、收银员、管理员三条线怎么交汇
系统里的角色就三类,别急着加权限,先把各自要做的事列清楚。收银员在收银台操作,负责办卡、结算、现场兑换;管理员在后台维护积分规则、查流水、处理异常;会员是积分数据的来源,每次消费和兑换都会在他的积分账户上留下一笔记录。三条线交汇的地方,就是那张积分流水表——谁、在什么时间、因为哪笔业务、积分变了多少、变动后余额是多少。
核心流程我用四个字概括:办、消、兑、清。办卡时系统生成唯一会员编号,初始积分为零;消费时按规则计算本单应得积分并写入账户;兑换时校验积分余额和商品库存,两边同时扣减;积分过期时由定时任务批量清零并留痕。这里有一个容易被忽略的设计点:积分清零也必须有流水记录。否则会员来投诉说「我积分怎么没了」,你连一条证据都拿不出来,对账全是糊涂账。把这条流水设计进去,答辩时主动讲出来,评委一般都会认可这个细节。
2.2 建库建表:member、product、rule、transaction、exchange一张不少
我见过不少课设把积分直接塞进会员表,商品表单独一张,积分变动完全不留历史,最后演示的时候功能倒是能跑,但评委一问「怎么证明这笔积分没送重复」就哑火。这五张表是这套系统的地基,直接复制到你的初始化SQL里就能用:
-- 1) 会员表:points 用 DECIMAL,绝不用 double CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_no VARCHAR(32) NOT NULL UNIQUE COMMENT '会员编号', name VARCHAR(50) NOT NULL COMMENT '会员姓名', phone VARCHAR(20) DEFAULT NULL COMMENT '手机号', points DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '当前可用积分', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_member_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表'; -- 2) 商品表:现金价格和兑换积分分开存 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT '现金售价', exchange_points DECIMAL(12,2) NOT NULL COMMENT '兑换所需积分', stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兑换商品表'; -- 3) 积分规则表:一条规则决定怎么送积分 CREATE TABLE points_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL COMMENT '规则名称', rule_type VARCHAR(20) NOT NULL COMMENT 'EARN=消费送积分 SPEND=兑换', min_amount DECIMAL(10,2) DEFAULT 0 COMMENT '消费满多少才积分', points_per_unit DECIMAL(10,4) NOT NULL COMMENT '每单位金额送多少分', valid_days INT DEFAULT 365 COMMENT '积分有效天数', status TINYINT DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分规则表'; -- 4) 积分流水表:账本的核心,所有积分变动都落这里 CREATE TABLE points_transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, trade_no VARCHAR(64) NOT NULL COMMENT '业务单号,幂等用', member_id BIGINT NOT NULL, type VARCHAR(20) NOT NULL COMMENT 'EARN/SPEND/EXPIRE', points_change DECIMAL(12,2) NOT NULL COMMENT '变动值,支出为负', balance_after DECIMAL(12,2) NOT NULL COMMENT '变动后的余额快照', ref_no VARCHAR(64) DEFAULT NULL COMMENT '关联订单号/兑换单号', remark VARCHAR(255) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_member_time (member_id, create_time), UNIQUE KEY uk_trade_no (trade_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分流水表'; -- 5) 兑换记录表:跟流水表互相对应 CREATE TABLE exchange_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, trade_no VARCHAR(64) NOT NULL, member_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, spent_points DECIMAL(12,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_member (member_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换记录表';这套设计的核心逻辑是「余额可查、流水可溯」。member表只保存当前余额,points_transaction表保存每一次变动,两个表通过member_id关联。balance_after字段存的是这笔交易完成后会员的剩余积分,这相当于给每笔流水拍了张快照,以后做对账或数据修复,直接看快照就能到推演整条链路。trade_no上建唯一索引,是为了防止同一笔订单被重复入账,这是积分系统里最隐蔽的坑——收银员手一抖多点一次提交,积分就多送了一倍。
参数上我特别解释两个地方。第一,points用DECIMAL(12,2)而不用double,是因为double是二进制浮点,算0.1+0.2会出现0.30000000000000004,积分账本里出现这种尾差,对账直接崩。DECIMAL的精度由decimal(12,2)里的12和2决定,能存到十亿级别,课设场景完全够用。第二,points_per_unit用DECIMAL(10,4),是因为规则可能是「消费满10元送1积分」,换算下来每元送0.1分,如果字段只留2位小数,0.1还能存,但遇到「满15元送2分」这种规则,每元0.1333分就存不下了,所以留4位小数做中间计算值。
提示:如果项目报告里要求画ER图,别把五张表全画成孤立矩形。member到points_transaction是一对多,product到exchange_record是一对多,points_rule可以挂在member旁边当配置表。把这三条关系画清楚,ER图这页基本就拿满了。
2.3 积分过期怎么落地:MySQL事件加存储过程,比Java定时任务更省事
积分都有有效期,这是业务常识,但很多课设代码里根本没实现,答辩时被问到就露馅。我一般用「MySQL事件+存储过程」来做过期清理,而不是在Java代码里写定时任务。原因是:数据库事件不依赖应用服务是否启动,代码里少一套调度逻辑,答辩时也容易解释,就说「过期是数据库层定时任务,跟业务代码解耦」。下面是完整脚本:
-- 开启事件调度器(MySQL 8.0 默认是 OFF,这条必须执行) SET GLOBAL event_scheduler = ON; -- 存储过程:把超期未用账户整体清零,并写一条 EXPIRE 流水 DELIMITER $$ CREATE PROCEDURE sp_expire_points() BEGIN DECLARE v_id BIGINT; DECLARE v_points DECIMAL(12,2); DECLARE done INT DEFAULT 0; -- 游标:取所有创建至今超过 365 天且仍有积分的会员 DECLARE cur CURSOR FOR SELECT id, points FROM member WHERE points > 0 AND create_time < DATE_SUB(NOW(), INTERVAL 365 DAY); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_points; IF done = 1 THEN LEAVE read_loop; END IF; -- 先落流水,再清零,方便对账 INSERT INTO points_transaction (trade_no, member_id, type, points_change, balance_after, remark) VALUES (CONCAT('EXP', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), '-', v_id), v_id, 'EXPIRE', -v_points, 0, '积分超期清零'); UPDATE member SET points = 0 WHERE id = v_id; END LOOP; CLOSE cur; END$$ DELIMITER ; -- 每天凌晨 2 点执行一次 CREATE EVENT IF NOT EXISTS evt_expire_points ON SCHEDULE EVERY 1 DAY STARTS CURRENT_TIMESTAMP + INTERVAL 1 DAY DO CALL sp_expire_points();这段脚本有几个值得注意的点。游标先查出来再逐条处理,是因为MySQL的游标结果集在OPEN时就固定了,不会因为循环里的UPDATE而重复读到同一行。先INSERT流水再UPDATE余额,保证了「清零」这个动作有据可查,哪怕UPDATE失败,流水里也能看到系统尝试过这笔操作。trade_no用「EXP+日期+会员id」拼出来,在刚才那张表的唯一索引约束下不会冲突。
我要提醒一句边界:这里的过期间隔写死为「注册时间起365天」,是课设场景的简化方案。真实的超市积分系统是按每笔积分入账时间逐批过期的,比如会员今天消费送的积分从今天算起一年内有效,而不是整个账户统一过期。答案里你可以这样简化,但要在报告里明确写一句「当前为简化方案,生产环境需按积分批次过期」,这反而显得你懂边界。
3. 把源代码里的核心链路写明白:消费送积分与积分兑换的实现
技术选型我建议用Spring Boot + MyBatis + Thymeleaf这套组合,不要碰SSH那套老古董。Spring Boot负责装配,MyBatis把SQL握在自己手里,Thymeleaf渲染页面,这个组合在Java课程设计里最常见,答辩时评委也熟悉,不会被追问冷门框架的细节。源代码拿到手后先别急着全部读,按层读:controller接收请求、service里写事务、mapper写SQL,思路立刻清晰。
3.1 消费结算与积分入账:事务边界要锁住,流水一条不能少
收银台结账是积分系统的入口,也是并发压力最大的操作。一笔消费进来,要做两件事:给member表加积分,给points_transaction表插流水。这两步必须在一个事务里,要么都成功,要么都回滚。我的核心代码长这样:
@Service public class PointsService { private final MemberMapper memberMapper; private final PointsRuleMapper ruleMapper; private final PointsTransactionMapper txMapper; public PointsService(MemberMapper memberMapper, PointsRuleMapper ruleMapper, PointsTransactionMapper txMapper) { this.memberMapper = memberMapper; this.ruleMapper = ruleMapper; this.txMapper = txMapper; } /** * 收银结算:按消费金额计算应得积分并完成入账 */ @Transactional(rollbackFor = Exception.class) public void settleOrder(SettleRequest request) { // 1. 先读当前会员和当前生效规则 Member member = memberMapper.selectById(request.getMemberId()); PointsRule rule = ruleMapper.selectByType("EARN"); // 2. 不满足最低消费门槛就返回 0 分 if (rule == null || request.getAmount().compareTo(rule.getMinAmount()) < 0) { return; } // 3. 积分 = 消费金额 * 每元送几分,四舍五入保留 2 位 BigDecimal earned = request.getAmount() .multiply(rule.getPointsPerUnit()) .setScale(2, RoundingMode.HALF_UP); if (earned.compareTo(BigDecimal.ZERO) <= 0) { return; } // 4. 乐观锁更新积分,版本号冲突说明数据被改过 int rows = memberMapper.addPointsByVersion( member.getId(), earned, member.getVersion()); if (rows == 0) { throw new BusinessException("会员信息已变更,请重新提交"); } // 5. 写流水:trade_no 用订单号保证幂等 PointsTransaction tx = new PointsTransaction(); tx.setTradeNo(request.getOrderNo()); tx.setMemberId(member.getId()); tx.setType("EARN"); tx.setPointsChange(earned); tx.setBalanceAfter(member.getPoints().add(earned)); tx.setRefNo(request.getOrderNo()); txMapper.insert(tx); } }这段代码的关键在@Transactional注解和乐观锁的组合。@Transactional(rollbackFor = Exception.class)把整个方法包成一个事务,第4步更新余额和第5步写流水只要有一个抛异常,两步全部回滚,数据库不会出现「积分加了但流水没记」的中间状态。这里注意rollbackFor必须指定,因为Spring默认只在遇到RuntimeException时才回滚,如果业务层抛的是自定义受检异常,不指定rollbackFor就会导致事务不回滚。
乐观锁对应的SQL是这样:
UPDATE member SET points = points + #{delta}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion}影响行数为0说明version变了,也就是这个会员的积分在读取之后被别的请求改过,此时直接抛异常让用户重试,而不是覆盖更新。这个场景放在收银台很合适——同一时间操作同一个会员的概率很低,用version判断足够,还省去了数据库行锁的开销。balanceAfter直接取「读到的旧积分+新增积分」,没有再查一次库,是合理的性能取舍。
3.2 积分兑换与库存扣减:先锁会员行,再锁商品行
兑换是扣减操作,比入账更怕并发问题。两个页面同时提交兑换请求,会员积分本来是100分,两个请求都读到100分,一个扣80一个扣60,最后余额变成负40,这就是典型的并发翻车现场。兑换场景和消费场景不同,兑换的并发量低但冲突概率高,所以我在这里用悲观锁,直接锁住数据库行:
/** * 积分兑换:扣积分 + 扣库存,任何一步失败全部回滚 */ @Transactional(rollbackFor = Exception.class) public ExchangeResult exchange(ExchangeRequest req) { // 1. 悲观锁锁定会员行,防止并发兑换把积分刷成负数 Member member = memberMapper.selectByIdForUpdate(req.getMemberId()); if (member == null) { throw new BusinessException("会员不存在"); } Product product = productMapper.selectByIdForUpdate(req.getProductId()); if (product == null || product.getStock() < req.getQuantity()) { throw new BusinessException("库存不足"); } BigDecimal cost = product.getExchangePoints() .multiply(BigDecimal.valueOf(req.getQuantity())) .setScale(2, RoundingMode.HALF_UP); // 2. 积分校验用 compareTo,别用 equals if (member.getPoints().compareTo(cost) < 0) { throw new BusinessException("积分不足"); } // 3. 扣积分、扣库存 memberMapper.addPoints(member.getId(), cost.negate()); productMapper.decreaseStock(product.getId(), req.getQuantity()); // 4. 写一条 SPEND 流水 + 一条兑换记录 PointsTransaction tx = new PointsTransaction(); tx.setTradeNo(req.getExchangeNo()); tx.setMemberId(member.getId()); tx.setType("SPEND"); tx.setPointsChange(cost.negate()); tx.setBalanceAfter(member.getPoints().subtract(cost)); tx.setRefNo(req.getExchangeNo()); txMapper.insert(tx); ExchangeRecord record = new ExchangeRecord(); record.setTradeNo(req.getExchangeNo()); record.setMemberId(member.getId()); record.setProductId(product.getId()); record.setQuantity(req.getQuantity()); record.setSpentPoints(cost); exchangeMapper.insert(record); return new ExchangeResult(record.getId(), cost); }selectByIdForUpdate对应的是SELECT * FROM member WHERE id = #{id} FOR UPDATE,这会让数据库锁定这一行,直到事务提交或回滚。期间其他事务想读或者改这行,都得排队等着。注意加锁顺序我固定为先member后product,这是防止死锁的关键。如果兑换A先锁member再锁product,兑换B先锁product再锁member,两边互相等对方释放,就死锁了。统一顺序能从根本上绕开这类问题,这个点值得在答辩时主动提一句。
积分比较用了compareTo而不是equals,这是一个非常容易忽略的细节。BigDecimal的equals方法会比较精度,new BigDecimal("1.0")和new BigDecimal("1.00")用equals比较结果是false,但它们的数值明明一样。金额计算过程中scale经常变化,所以一律用compareTo比较数值大小。项目中凡是涉及金额、积分判断的地方,我建议都用compareTo。
3.3 答辩时怎么解释这套架构:三层结构、事务与连接池的边界
很多同学代码跑通了,但被问「为什么分三层」就卡壳。这套系统的分层逻辑很清晰:Controller层只接收参数和返回结果,不写任何业务判断;Service层放事务和业务规则,比如算积分、校验库存;Mapper层只跟SQL打交道。三层各干各的事,好处是改动某一层不影响另外两层。比如想把积分规则从「每元送1分」改成「每10元送3分」,只需要改points_rule表里的数据,Java代码一行不用动,这就是规则跟代码解耦的实际价值。
连接池参数也值得在报告里写一笔。Spring Boot默认的HikariCP参数在课设环境够用,但答辩老师如果问「为什么这么配」,你要能答上来。我一般用这张表:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| maximum-pool-size | 10 | 课设并发量小,10个连接足够,多了浪费内存 |
| minimum-idle | 2 | 保留2个空闲连接,避免冷启动延迟 |
| connection-timeout | 30000 | 30秒拿不到连接就报错,防止请求无限等待 |
这里有个容易踩的认知误区:连接池不是越大越好。每个连接都对应数据库的一个线程和一份内存,你开50个连接,数据库就得多分配50份资源,反而拖慢性能。课设系统通常只有几个页面和几百条测试数据,10个连接完全够用,把计算资源留在业务逻辑上才是正解。
4. 把课设跑通到能答辩:4条避坑硬记录
这一章每条都是我实际跑课设时踩过的坑,不是玄学。现象、原因、解决三步写清楚,你照着排查能省下大量调试时间。这些坑有一个共同特点:代码编译期和普通功能测试时完全看不出来,只有数据量上来或者并发操作时才会暴露。
4.1 积分出现0.30000000000004:double的精度坑
现象:积分余额出现一长串小数尾差,比如200积分经过几次累加后变成200.00000000000003,会员查询积分时页面显示一串奇怪的数字,对账报表怎么都对不上。
原因:Java的double和数据库的float/double都是二进制浮点数,二进制无法精确表示0.1这种十进制小数,所以0.1+0.2的结果是0.30000000000000004。积分账本对精度极其敏感,任何尾差都会在汇总统计时被放大。
解决:数据库字段全部用DECIMAL,Java代码里所有积分和金额变量用BigDecimal,计算时统一setScale(2, RoundingMode.HALF_UP)保留两位小数。我给自己定的代码规范是:项目里出现float和double处理金额的,直接打回重写。这条规则帮我避掉了后面报表模块的大量返工。
4.2 并发兑换把积分扣成负数:忘了加锁
现象:用两个浏览器标签页同时登录同一个会员账号,同一秒提交两个兑换请求,每个请求都校验通过了,最后积分余额变成负数。
原因:校验积分和扣减积分是两条独立的SQL,两个请求同时读取积分余额时都读到100分,各自都认为可以扣80分,先各自扣完,最后数据库里就出现了负数。这就是典型的读-改-写竞态条件,问题出在「先查询再更新」的间隙。
解决:两个办法任选其一。一是用SELECT ... FOR UPDATE悲观锁锁住会员行,串行化处理同一个会员的兑换请求;二是直接用条件更新SQL,让数据库自己判断余额是否足够:
UPDATE member SET points = points - #{cost} WHERE id = #{id} AND points >= #{cost}这条SQL把「检查余额」和「扣减积分」合并成一条原子操作,数据库层面保证不会扣成负数,影响行数为0就说明余额不足。如果还要同时扣库存,记得把这条UPDATE和插流水、扣库存放在同一个事务里。
4.3 写好了定时事件却一次都不跑:事件调度器没开
现象:存储过程和事件都创建成功了,SHOW EVENTS也能看到记录,但积分就是不过期,手动调用存储过程又能正常执行。
原因:MySQL 8.0的event_scheduler参数默认是OFF,事件虽然创建了,但调度器根本没启动,自然不会执行定时任务。很多新手不知道这个开关,排错排半天。
解决:执行SET GLOBAL event_scheduler = ON;开启调度器,用SHOW VARIABLES LIKE 'event_scheduler';确认状态是ON。注意这个设置是临时的,MySQL重启后失效,要在my.cnf的[mysqld]段加一行event_scheduler=ON永久生效。另外如果一个事件卡住了,可以用ALTER EVENT evt_expire_points DISABLE;先停掉再排查,不用删掉重建。
4.4 本地能跑、答辩机器上连不上:时区与连接串的玄学
现象:项目在本地运行正常,传到答辩电脑上启动时报The server time zone value '?й???????' is unrecognized,或者中文乱码,偶尔还报Public Key Retrieval is not allowed,看着像数据库连接串的问题,但不知道动哪里。
原因:MySQL 8.0对时区很敏感,服务器默认时区不是中国时区;连接串缺了编码参数导致中文乱码;allowPublicKeyRetrieval默认关闭,MySQL 8.0的caching_sha2_password认证插件会拒绝连接。
解决:JDBC连接串把参数写全,一次到位:
jdbc:mysql://localhost:3306/pointsdb?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true逐个参数说:useUnicode=true配合characterEncoding=utf8保证中文不乱码;serverTimezone=Asia/Shanghai解决时区报错;useSSL=false关闭本地开发的SSL握手,省去证书配置;allowPublicKeyRetrieval=true是为MySQL 8.0的认证插件准备的,不加会报公钥检索错误。这套参数是我踩了两次坑后固定下来的标准模板,现在所有课设项目都直接抄这套。
5. 把「能跑」变成「会讲」:一套演示脚本与三个加分扩展
5.1 演示顺序:按「办卡→消费→查流水→兑换→过期」五步走
答辩演示时,功能顺序比功能多少更重要。我的习惯是严格按业务链路走:先演示新会员办卡,让评委看到member_no自动生成、初始积分为0;再模拟一笔真实消费,输入金额,页面显示本次获得积分,数据库里多了一条EARN流水;接着演示积分兑换,选一个商品,确认积分余额和库存同步减少;然后切到会员详情页,按时间倒序展示积分流水,特意指一下balance_after列,告诉评委「这里存了每次变动后的余额快照,方便对账」;最后手动执行CALL sp_expire_points();,演示一条EXPIRE流水被写入。每一步操作前先说「我要做什么、预期看到什么」,评委跟着你的思路走,就不会盯住某个边角功能不放。
5.2 三个加分扩展:从课设作业变成面试作品
如果时间和精力允许,我会建议在基础功能之外挑一个扩展点做深。
| 扩展点 | 技术方案 | 答辩怎么讲 |
|---|---|---|
| 登录与权限 | Spring Security + BCrypt密码加密 | 管理员和收银员分角色登录,积分规则只有管理员能改 |
| 积分实时查询 | Redis缓存会员积分余额 | 积分是读多写少的场景,缓存能扛住高并发查询,写操作再更新缓存 |
| 数据可视化 | ECharts + 聚合查询SQL | 按日统计积分发放量和消耗量,用折线图展示趋势 |
这三个扩展都不需要改表结构,只是在现有代码上加一层。第一个扩展对应权限控制,第二个对应性能优化,第三个对应数据分析,分别覆盖了项目报告里「功能性需求、非功能性需求、系统亮点」三个章节。我当年就是只顾着把demo跑通就冲上去答辩,结果被评委一句话问住:积分过期你怎么处理?那次之后我养成一个习惯:动手写代码前,先把自己这版方案里最容易被追问的三个边界问题列出来,写进报告里。这套思路后来帮我避了不少坑,今天整理出来,希望帮到你。
本文还有配套的精品资源,点击获取