简介:这份乡镇自来水收费系统源码是一套面向Java毕业设计或课程设计的完整项目,基于SSM框架与JSP前端技术,搭配MySQL 5.7数据库和Tomcat 7运行环境,适合需要快速搭建管理类Web系统、理解Spring+SpringMVC+MyBatis整合流程的学生开发者。系统围绕水费收缴业务,划分为管理员与用户两类角色:管理员可维护水表档案、审核更换水表请求、执行抄表与缴费管理,并处理公告、留言及用户信息;用户则可在线提交换表申请、自助缴费、查看公告与留言、修改个人资料,功能覆盖面较完整。包内共1942个文件,以JavaScript、CSS、JSP等前端资源,Java业务类与XML映射文件,以及SQL数据库脚本、Eclipse/Idea工程配置为主,另有说明文档和LW论文文档,总计42.56MB,便于直接导入部署或对照学习。目前已有60人学习下载,适合作为课设答辩或毕设二次开发的基础参考,尤其是希望获得完整前后端代码和数据库设计思路的读者。
1. 乡镇自来水收费系统这套 SSM+JSP+MySQL 源码,解决的不只是毕业设计
乡镇自来水厂的收费场景和城市水务公司完全不同:用户规模从几百户到一万户出头,抄表员按片区手工抄表,月底对着 Excel 台账算钱,错账漏账全靠人工兜底。一套 SSM+JSP+MySQL 的乡镇自来水收费系统,核心是把「户号—水表—抄表记录—账单—缴费」这条链路用数据库固定下来,让月末结算从两天手工核对压到一次批处理。
三个技术词各管一段:SSM 负责业务分层和事务控制,JSP 承担录入与查询页面,MySQL 存住账目并做月末批量出账。对做毕业设计的人来说,这是业务真实、前后端都有工作量的选题;对刚入行 Java 的人,它又是一套练三层架构、动态 SQL、存储过程和 Tomcat 部署的最小完整系统。下面按数据库建模、SSM 主流程、MySQL 批量出账、部署验收四个环节展开。
2. 数据库设计:乡镇自来水收费系统的表、字段与约束怎么定
数据模型决定这套系统能撑到多少户,也决定答辩时被问「为什么这么建表」能不能答上来。乡镇收费系统的共性是用户、水表、抄表、账单拆成独立表,而不是每户一行把读数和金额全塞进去。拆开的代价是查询多一次 join,收益是换表、调价、补抄、退费这些真实操作都有地方落。
2.1 用户、水表、抄表三张核心表:一户多表如何建模
t_user 存的是「谁该交钱」,包括户号、户名、地址和用户类型;t_meter 存的是「哪块表在计量」,通过 user_id 挂回用户;t_reading 存的是「每个账期读到了什么数」,挂在 meter_id 而不是 user_id 上。三张表分开的关键理由是乡镇场景里一户多表很常见:井水和自来水分开计量,或者一宅两表。如果读数挂在用户下,换表后历史数据会跟着旧表一起丢。
抄表记录里同时存 this_reading 和 last_reading,last_reading 在写入时由最近一条记录反查得到。这种冗余用空间换查询便利,计费时不需要再逐条比较相邻记录。整张表上最重要的约束是 (meter_id, period) 的唯一索引,它保证同一块表同一个账期只能有一条有效记录,是防重复抄表的第一道闸门。
2.2 费率与账单表:阶梯水价的版本化设计
费率不要写死在 Java 常量里。t_tariff 用 period 表示生效账期,tier_no 表示第几档,配合 min_usage、max_usage 描述阶梯区间。调价时插入一条新 period 的记录,历史账单按旧费率保留,这比改代码重新部署稳得多。对最常见的两档阶梯,费率表里每个账期就是两行数据。
账单表 t_bill 上的 (user_id, period) 唯一索引承担防重复出账的职责。status 字段区分未缴、已缴、欠费三种状态,不要用 paid_amount 是否为零去反推状态——零金额可能是减免单,也可能是还没生成账单。滞纳金不建议直接改原账单金额,单独记录或另建罚金字段,方便对账。
2.3 建表 SQL 与三个常见设计失误
下面这组 DDL 是这套系统最常复用的部分,字段按最小清单设计,足够跑通抄表、计费、缴费和月度报表:
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, user_no VARCHAR(20) NOT NULL COMMENT '户号,对外可见', user_name VARCHAR(50) NOT NULL COMMENT '户名', address VARCHAR(200) COMMENT '用水地址', phone VARCHAR(20), user_type TINYINT NOT NULL DEFAULT 0 COMMENT '0居民 1商业', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在用 0停用', UNIQUE KEY uk_user_no (user_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用水户'; CREATE TABLE t_meter ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '归属用户', meter_no VARCHAR(30) NOT NULL COMMENT '表编号', install_date DATE, init_reading DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '装表底度', current_reading DECIMAL(10,2) COMMENT '最近一次抄表读数', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停用', UNIQUE KEY uk_meter_no (meter_no), KEY idx_meter_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='水表'; CREATE TABLE t_reading ( id INT PRIMARY KEY AUTO_INCREMENT, meter_id INT NOT NULL, period CHAR(6) NOT NULL COMMENT '账期,如202406', this_reading DECIMAL(10,2) NOT NULL COMMENT '本次读数', last_reading DECIMAL(10,2) NOT NULL COMMENT '上次读数', usage_amount DECIMAL(10,2) NOT NULL COMMENT '本期用量=本次-上次', read_date DATE, reader VARCHAR(20) COMMENT '抄表员', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待计费 1已计费', UNIQUE KEY uk_reading (meter_id, period), KEY idx_reading_period_status (period, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抄表记录'; CREATE TABLE t_tariff ( id INT PRIMARY KEY AUTO_INCREMENT, tariff_name VARCHAR(30) COMMENT '费率名称', user_type TINYINT COMMENT '0居民 1商业', period CHAR(6) NOT NULL COMMENT '生效账期', tier_no INT NOT NULL COMMENT '阶梯序号,从1开始', min_usage DECIMAL(10,2) NOT NULL DEFAULT 0, max_usage DECIMAL(10,2) COMMENT 'NULL表示上不封顶', price DECIMAL(8,4) NOT NULL COMMENT '单价:元/吨', KEY idx_tariff_period (period, user_type, tier_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='费率版本'; CREATE TABLE t_bill ( id INT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(40) NOT NULL, user_id INT NOT NULL, period CHAR(6) NOT NULL, usage_amount DECIMAL(10,2) NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT '应收金额', paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT '0未缴 1已缴 2欠费', due_date DATE, create_time DATETIME, UNIQUE KEY uk_bill (user_id, period), KEY idx_bill_status (status, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='水费账单';表之间的关系和关键字段可以浓缩成一张对照表,答辩画 ER 图时直接照着画:
| 表 | 挂靠关系 | 唯一约束 | 一句话职责 |
|---|---|---|---|
| t_user | 无 | user_no | 谁该交钱 |
| t_meter | user_id → t_user | meter_no | 哪块表在计量 |
| t_reading | meter_id → t_meter | (meter_id, period) | 每个账期读到的数 |
| t_tariff | 无 | 无,按 period 取版本 | 每吨多少钱、几档 |
| t_bill | user_id → t_user | (user_id, period) | 一笔应收账目 |
这套建模最容易犯的三个失误:
- t_reading 不加唯一索引,重复提交或并发双击会直接产生两笔账,后面对账全是坑;
- 金额字段用 FLOAT 或 DOUBLE,几千条数据看不出问题,累计上万条后小数位对不上,必须用 DECIMAL(10,2),Java 侧用 BigDecimal 参与运算;
- period 用 DATETIME 存每月 1 号,查询反而要先 DATE_FORMAT 再比对,直接用 CHAR(6) 存 '202406',排序、区间查询、分组都走字符串比较,索引也能命中。
提示:以上建表语句在 MySQL 5.7 和 8.0 都能用。如果本地装的是 MySQL 8.0,注意驱动类名和时区参数,部署章节会专门讲。
3. SSM 三层架构把抄表、计费、缴费串成可维护的主流程
3.1 包结构分层与 Controller 只做参数收口
SSM 项目最怕的是把 SQL 写在 JSP 里、把逻辑堆在 Controller 里。这套系统按下面这样分包:
com.water ├── controller // URL 入口,只做参数绑定和结果封装 │ ├── UserController.java │ ├── ReadingController.java │ └── BillController.java ├── service // 业务规则,事务边界开在这里 │ ├── ReadingService.java │ └── BillService.java ├── mapper // MyBatis 接口与 XML │ ├── ReadingMapper.java │ └── ReadingMapper.xml ├── pojo // 实体,与表一一对应 ├── common // Result、PageBean、BizException └── config // Spring、SpringMVC 配置类Controller 的角色是参数收口,不做任何业务判断。以抄表录入为例,前端 JSP 表单提交 meterId、period、thisReading 三个参数,Controller 把异常翻译成前端能展示的文案:
@Controller @RequestMapping("/reading") public class ReadingController { @Autowired private ReadingService readingService; @RequestMapping("/save") @ResponseBody public Result save(@RequestParam Integer meterId, @RequestParam String period, @RequestParam BigDecimal thisReading) { try { readingService.saveReading(meterId, period, thisReading); return Result.ok("抄表成功"); } catch (DuplicateKeyException e) { // 唯一索引 (meter_id, period) 兜住重复提交 return Result.fail("该水表本账期已抄过,请核对后再提交"); } catch (BizException e) { return Result.fail(e.getMessage()); } } }需要说明的是,@RequestParam 会在参数缺失时直接抛异常,前端 JSP 的表单字段名必须和这里一致。Result 是统一返回体,包含 code、message、data 三个字段,JSP 里用 Ajax 拿到 code 后决定弹提示还是刷新列表。测试时最常遇到的 400 错误,基本都是表单字段名和这里对不上。
3.2 抄表录入:MyBatis 动态 SQL 与重复提交拦截
抄表的核心逻辑在 Service 层:先反查上次读数,校验本次读数合法性,再插入记录并更新水表当前读数。整个方法加 @Transactional,插入失败时水表读数不会留下半截数据:
@Transactional(rollbackFor = Exception.class) public void saveReading(Integer meterId, String period, BigDecimal thisReading) { Meter meter = meterMapper.selectById(meterId); BigDecimal lastReading = readingMapper.selectLastReading(meterId); // 校验:本次读数小于上次底度,一般是抄错或换表未登记 if (thisReading.compareTo(lastReading) < 0) { throw new BizException("本次读数小于上次底度:" + lastReading); } readingMapper.insertReading(meterId, period, thisReading, lastReading); // 同步水表当前读数,仅用于列表展示,不作为计费依据 meterMapper.updateCurrentReading(meterId, thisReading); }selectLastReading 的实现是「按 meter_id 取最近一条记录」,按 id 倒序 LIMIT 1。比用 MAX(period) 更稳妥,因为补录的历史记录不会干扰当前底度的计算。
对应的 Mapper XML 如下,插入语句直接在本期与上期之间做减法得到本期用量,避免在 Java 侧多传一个参数:
<insert id="insertReading" parameterType="map"> INSERT INTO t_reading (meter_id, period, this_reading, last_reading, usage_amount, read_date, status) VALUES (#{meterId}, #{period}, #{thisReading}, #{lastReading}, #{thisReading} - #{lastReading}, NOW(), 0) </insert> <update id="updateReadingStatus" parameterType="list"> UPDATE t_reading SET status = 1 WHERE id IN <foreach collection="list" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>#{thisReading} - #{lastReading}预编译后是? - ?,MySQL 会直接算出数值,这是 MyBatis 的常见写法。updateReadingStatus 里的 用于批量回写抄表状态,参数是账单生成后的抄表记录 id 集合。手写 SQL 时注意 foreach 的 collection 属性值,接口方法参数要用 @Param("list") 标注,否则 MyBatis 找不到集合。
接口方法签名与 XML 的对应关系,可以用一张小表说明:
| 接口方法 | XML id | 说明 |
|---|---|---|
| insertReading(...) | insertReading | 插入抄表记录 |
| selectLastReading(Integer meterId) | selectLastReading | 取最近一次底度 |
| updateReadingStatus(List<Integer> ids) | updateReadingStatus | 批量回写为已计费 |
3.3 阶梯计费在 Service 侧的分段计算
水价最常见的两档或三档阶梯,分段逻辑放在 Service 里比放在 SQL 里好维护。下面方法按 t_tariff 的累计上限计算:假设 0 到 20 吨每吨 2.8 元、20 到 30 吨每吨 4.2 元、30 吨以上每吨 6.5 元,tier_no 从 1 递增,max_usage 存该档累计上限,最后一档 max_usage 为 NULL:
/** * tariffs 按 tier_no 升序,max_usage 为 NULL 的档作为最后一档(不封顶) */ public BigDecimal calcAmount(BigDecimal usage, List<Tariff> tariffs) { BigDecimal amount = BigDecimal.ZERO; BigDecimal prevMax = BigDecimal.ZERO; // 上一档的累计上限 for (Tariff tier : tariffs) { if (usage.compareTo(prevMax) <= 0) { break; // 用量不超过上一档上限,后续档不参与 } BigDecimal upper = tier.getMaxUsage() == null ? usage : tier.getMaxUsage(); BigDecimal inTier = usage.min(upper).subtract(prevMax); amount = amount.add(inTier.multiply(tier.getPrice())); prevMax = upper; } return amount.setScale(2, RoundingMode.HALF_UP); }关键参数是 prevMax,它累计了前面所有档的上限。以 25 吨为例:第一档 upper=20,inTier=20,累计 56 元,prevMax 变成 20;第二档 usage 25 大于 prevMax 20,upper=30,inTier=min(25,30)-20=5,再加 21 元,总计 77 元。如果费率行没按 tier_no 排序,结果会错,所以查询一定要 ORDER BY tier_no。
提示:金额计算全程用 BigDecimal 的 compareTo、subtract、multiply,不要用 == 或直接加减。float 的精度问题在月度报表汇总时会直接暴露。
4. 月末批量出账:MySQL 存储过程与联合索引的实战用法
4.1 为什么月末结算不循环调 Service
一个月结一次账,几千条抄表记录要生成对应账单。常见做法是在 Java 里 for 循环调 BillService 逐条 insert。这种写法在小乡镇能用,但每一条 insert 都是一次网络往返和一次单独事务,一万户的量级会明显变慢,而且一旦中途失败,回滚的是单条而不是整个账期。
存储过程的优势在于校验、汇总、插入、回写状态放在同一个数据库事务里,只发一次调用,事务边界完整。劣势是调试困难、版本管理弱。所以职责要划清楚:阶梯计费这类要排序、要计算的分段逻辑留在 Java 侧;存储过程只做四件确定性高的事——检查该账期是否已出账、按用户汇总用量、按基准价生成账单、回写抄表状态。两条路线对比如下:
| 方案 | 事务边界 | 一万户耗时量级 | 调试难度 | 适用规模 |
|---|---|---|---|---|
| Java 循环调 Service | 单条插入各自事务 | 分钟级 | 低 | 千户以内 |
| 存储过程批量出账 | 整个账期一个事务 | 秒级 | 中 | 千户以上 |
| Java 批量提交 ExecutorType.BATCH | 一个事务分批提交 | 秒级 | 中 | 两者之间 |
4.2 关账存储过程:先防重复再批量写账
下面这个存储过程是月末核心。它先检查目标账期是否已有账单,有就直接报错终止;没有则按用户聚合待计费抄表记录,生成账单,并把抄表记录回写为已计费:
DELIMITER $$ CREATE PROCEDURE sp_generate_bill( IN p_period CHAR(6), -- 账期,如 '202406' IN p_due_days INT -- 缴费期限天数,一般传 30 ) BEGIN DECLARE v_count INT DEFAULT 0; DECLARE v_price DECIMAL(8,4); START TRANSACTION; -- 防重复出账:目标账期已有任何账单,直接终止本次操作 SELECT COUNT(*) INTO v_count FROM t_bill WHERE period = p_period; IF v_count > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'period already billed'; END IF; -- 取第一档单价作为基准价;阶梯差额计算见 3.3 的 Java 算法 SELECT price INTO v_price FROM t_tariff WHERE period = p_period AND tier_no = 1; -- 按用户聚合待计费抄表量,生成账单 INSERT INTO t_bill (bill_no, user_id, period, usage_amount, amount, status, due_date, create_time) SELECT CONCAT(p_period, '-', u.user_no), u.id, p_period, t.usage_amount, t.usage_amount * v_price, 0, DATE_ADD(CURDATE(), INTERVAL p_due_days DAY), NOW() FROM ( SELECT m.user_id, SUM(r.usage_amount) AS usage_amount FROM t_reading r JOIN t_meter m ON r.meter_id = m.id WHERE r.period = p_period AND r.status = 0 GROUP BY m.user_id ) t JOIN t_user u ON t.user_id = u.id; -- 回写抄表状态,防止再次累计 UPDATE t_reading SET status = 1 WHERE period = p_period AND status = 0; COMMIT; END$$ DELIMITER ;调用方式:
CALL sp_generate_bill('202406', 30);参数说明:p_period 是 CHAR(6) 账期,p_due_days 控制账单缴费截止日。WHERE 里同时带 period 和 status,直接命中第 2 章建的联合索引 idx_reading_period_status。如果该账期已出过账,第二次调用会看到 SQLSTATE 45000 的报错,这本身就是防重复的验收点。
如果项目确实要阶梯计费,有两种演进路径:一是在这里把子查询换成按 tier_no 分段的三段 CASE,二是把 3.3 计算好的金额写入扩展列 amount_calc,由这个过程读取落账。前者简单但调价要改存储过程,后者多一张表的成本但费率完全可配置,更符合 2.2 的版本化设计思路。
在 Java 侧,关账任务用 Spring 定时任务触发,JdbcTemplate 直接调用存储过程:
@Component public class MonthCloseTask { @Autowired private JdbcTemplate jdbcTemplate; // cron: 秒 分 时 日 月 周;每月 1 日 00:30 关上一账期 @Scheduled(cron = "0 30 0 1 * ?") public void closeLastMonth() { String period = lastPeriod(); // 自行实现:取上个月账期字符串 jdbcTemplate.update("CALL sp_generate_bill(?, ?)", period, 30); log.info("账期 {} 出账完成", period); } }注意 @Scheduled 需要任务扫描生效,在配置类上加 @EnableScheduling。定时关账适合测试环境验证,生产环境怕定时任务误触,改成管理员在页面上手动点「月末关账」更稳妥,这也是答辩时能讲的业务取舍。
4.3 联合索引 (period, status) 与分页查询的 Explain 验证
第 2 章已经为 t_reading 声明了 idx_reading_period_status(period, status)。这个索引同时服务两类查询:出账时找「某账期未计费记录」,以及页面展示「某账期抄表明细」。用 EXPLAIN 验证是否命中:
EXPLAIN SELECT * FROM t_reading WHERE period = '202406' AND status = 0;type 应为 ref,key 显示 idx_reading_period_status,rows 远小于全表。如果当初只建了 period 单列索引,status 的过滤要靠回表,rows 会大一个数量级。
账单分页查询同样要注意过滤和排序的组合。下面这条 SQL 在 JSP 的「欠费列表」页面很常见:
SELECT u.user_no, u.user_name, b.period, b.amount, b.status FROM t_bill b JOIN t_user u ON b.user_id = u.id WHERE b.status = 2 ORDER BY b.create_time DESC LIMIT #{offset}, #{pageSize};配合 idx_bill_status (status, create_time),过滤和排序能共用一个索引,避免 filesort。LIMIT 深分页在乡镇数据量下问题不大,如果被问到性能边界,可以答「数据量到几十万后再改基于游标的 keyset 分页」,这是一个加分点。
提示:索引不是越多越好。t_reading 上 (meter_id, period) 负责防重,idx_reading_period_status 负责出账与查询,两条足够。再多加索引会让写入变慢,抄表录入恰恰是这套系统写操作最多的场景。
5. 部署到 MySQL+Tomcat 与答辩验收:配置改动、故障排查和造数压测
5.1 war 包部署的四个必改配置
war 包放进 Tomcat webapps 目录启动即可,但有四个配置不改正就会在答辩现场翻车。第一,JDK 8 + MySQL 8 必须用驱动类名 com.mysql.cj.jdbc.Driver,老驱动只兼容 MySQL 5.x;第二,连接串要带 serverTimezone 和 characterEncoding,否则连不上或中文乱码;第三,jdbc.properties 的账号密码要和本地库一致;第四,清掉 Tomcat work 目录下旧 JSP 编译产物,否则改了页面不生效。
# 1. 停掉 Tomcat,避免 war 解压冲突 sudo systemctl stop tomcat9 # 2. 复制 war 到 webapps,war 文件名决定访问路径 cp water.war /opt/tomcat/webapps/ # 3. 清空 work 缓存,保证 JSP 重新编译 rm -rf /opt/tomcat/work/Catalina/localhost/water # 4. 启动并跟踪日志,看到 "Server startup" 再访问 sudo systemctl start tomcat9 tail -f /opt/tomcat/logs/catalina.out访问路径就是 http://localhost:8080/water/ 。测试期把日志级别调到 DEBUG,MyBatis 的 SQL 会打出来,接口报错时定位最快。
5.2 答辩现场故障优先级表
| 现象 | 定位思路 | 处理方式 |
|---|---|---|
| 启动报 ClassNotFoundException: com.mysql.jdbc.Driver | MySQL 驱动类名不匹配 | jdbc.properties 改 com.mysql.cj.jdbc.Driver,并升级 mysql-connector-java 8.x |
| 启动成功但页面查不到数据 | 库没导入或配置不对 | 确认 water 库存在,url/username/password 与本地一致 |
| JSP 页面中文乱码 | 请求编码或字符集不对 | 连接串加 characterEncoding=utf8,表统一 utf8mb4,配 CharacterEncodingFilter |
| 功能改了页面没变化 | Tomcat 缓存旧 JSP | 删 work 目录重启,或删掉旧 war 重新部署 |
| 存储过程权限报错 | 应用账号无 EXECUTE 权限 | 用 root 执行一次 CALL,再给账号授权 |
5.3 用一条 SQL 造一万条抄表数据压测月末出账
答辩前最好验证月末出账性能。手工录一万条不现实,用数字表批量插入:利用 information_schema.columns 的笛卡尔积生成序号,把一万条待计费记录分散到 200 块表上:
INSERT INTO t_reading (meter_id, period, this_reading, last_reading, usage_amount, read_date, status) SELECT 1 + (n % 200), -- 200 块表轮询分配 DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y%m'), 200 + n, 100 + n, 100, CURDATE(), 0 FROM ( SELECT (@i := @i + 1) AS n FROM information_schema.columns, (SELECT @i := 0) r LIMIT 10000 ) t;然后调用关账存储过程并再次调用验证防重复:
CALL sp_generate_bill( DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y%m'), 30 ); -- 再次调用同一账期,应看到 SQLSTATE 45000 报错 CALL sp_generate_bill( DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y%m'), 30 );最后对账:
SELECT COUNT(*) FROM t_bill WHERE status = 0; SELECT COUNT(*) FROM t_reading WHERE status = 0 AND period = DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y%m');两条结果都为 0,说明出账与回写一致。
本文还有配套的精品资源,点击获取