简介:这是一套面向计算机专业本科生的高分毕业设计项目源码,适用于毕设选题、课程设计与期末大作业,聚焦画师与客户间的在线约稿协作场景,解决作品展示、需求发布、订单管理与资金对接等核心业务问题。资源包共619个文件,涵盖139个Java后端逻辑文件、101个Vue前端页面组件、59个JPG作品图及UI素材、41个JS交互脚本、18个XML配置与1个SQL数据库脚本,完整支撑SpringBoot+Vue全栈开发学习与部署实践;压缩包大小22.63MB,结构清晰,含bat一键启停脚本与.bak备份文件,便于调试溯源。已有80人下载学习,用户可直接导入IDEA与Navicat运行,获得可演示的前后端分离系统、规范化数据库设计、响应式管理界面及真实业务流程闭环,是掌握Java Web开发、MySQL建模与Vue组件化实践的优质实操案例。
1. 这不是又一个“毕设 CRUD”,而是一个真实可跑、带交易闭环的画师约稿平台:Java+SpringBoot 后端扛住并发下单,Vue 前端实现作品展示/沟通/订单状态机,MySQL 存下画师资质、稿件版本、支付流水三类强一致性数据
你下载到的这个.zip文件,表面看是“高分毕设项目”,但拆开后你会发现:它没用 H2 内存库凑数,没把支付逻辑写成System.out.println("模拟支付成功"),也没把用户上传的原图直接File.save()到src/main/resources/static下——它真在用 MinIO(或本地文件服务)存原图,真在用 MySQL 的transaction_isolation = REPEATABLE-READ保证「画师接单→买家付款→平台冻结佣金→画师交稿→买家确认→佣金结算」这条链路上,任意环节失败都能回滚;它前端 Vue 里router-view套了 4 层嵌套路由,只为让「个人主页→作品墙→约稿页→聊天窗口」切换不刷新页面;它后端 SpringBoot 的@Transactional不只打在 Service 方法上,还配合@Retryable处理第三方消息推送失败。这不是教学 Demo,是能跑通「画师入驻审核→作品发布→买家询价→签订电子协议→分阶段付款→交付验收→评价返佣」全链路的最小可行产品(MVP)。适合两类人:一是计算机专业大四学生,拿它当毕设基座,删掉冗余模块、换掉 UI 组件、补上自己学校要求的文档,两周就能过答辩;二是刚转行的 Java/Vue 全栈新人,把它当「带业务语义的 SpringBoot+Vue 实战手册」,从pom.xml依赖冲突怎么解,到vue.config.js代理跨域怎么配,再到 MySQL 表字段为什么设TINYINT(1)而不是BOOLEAN,每一步都踩在真实开发的刀刃上。
2. 搭建环境:从 JDK 17 到 Vue CLI 5,避开 JDK 版本错配、Node 模块缓存污染、MySQL 时区错乱这三座大山
2.1 JDK 17 + SpringBoot 3.x 是硬性前提:别用 JDK 8 强行降级,也别用 JDK 21 试水
这个项目pom.xml中<spring-boot.version>3.2.0</spring-boot.version>明确锁定了 SpringBoot 3.x,而 SpringBoot 3.x强制要求 JDK 17+(JDK 21 也可,但需额外配置spring.config.import=optional:classpath:/application-jdk21.yml)。如果你本地装的是 JDK 8,mvn clean compile会直接报错:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project painter-platform: Fatal error compiling: java.lang.IllegalAccessError: class org.springframework.boot.gradle.plugin.JavaPluginFeatures (in unnamed module @0x1a2c9b6e) cannot access class com.sun.tools.javac.tree.JCTree$JCExpression (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.tree to unnamed module @0x1a2c9b6e -> [Help 1]这不是插件问题,是 JDK 8 的javacAPI 在 JDK 17+ 中被模块化隔离了。正确做法:
- 卸载 JDK 8,安装 Adoptium Temurin 17.0.10+7 (LTS 版本,社区验证最稳);
- 设置
JAVA_HOME指向新 JDK 根目录(如C:\Program Files\Eclipse Adoptium\jdk-17.0.10+7); - 在项目根目录执行
mvn -v,确认输出中Java version: 17.0.10且Maven home: ...路径无空格(Windows 用户尤其注意路径含中文或空格会导致maven-compiler-plugin加载失败)。
提示:SpringBoot 3.x 移除了
spring-boot-starter-web中的spring-boot-starter-tomcat,改用spring-boot-starter-webflux默认嵌入 Netty。如果你看到启动日志里Tomcat started on port(s): 8080,说明你误用了 SpringBoot 2.x 的 starter,必须检查pom.xml是否混入了旧版依赖。
2.2 Vue 项目必须用 Node 18.17.0 + npm 9.6.7:高版本 npm 会破坏node_modules符号链接
项目前端目录frontend/下的package.json指定"vue": "^3.3.4",这是 Vue 3.3 系列,对 Node 版本敏感。实测 Node 20.x 会导致npm install后node_modules/.vite/deps目录缺失,vite dev启动时报错Cannot find module 'vue';Node 16.x 则因esbuild插件兼容性问题,在vite build阶段卡死在transforming (esbuild)。血泪经验:
- 下载 Node.js 18.17.0 LTS (不要选 Current 版本);
- 安装后执行
npm install -g npm@9.6.7(npm -v必须为9.6.7); - 进入
frontend/目录,先删光node_modules和package-lock.json,再执行npm install—— 这步不能省,否则旧 lockfile 会保留已废弃的@vue/compiler-sfc@3.2.47,导致vite编译时defineProps类型推导失败。
2.3 MySQL 8.0.33 是唯一兼容版本:时区、JSON 字段、全文索引缺一不可
项目 SQL 脚本sql/painter_platform.sql中大量使用:
JSON类型字段(如artist_profile表的skills字段存["厚涂","赛博朋克","Q版"]);FULLTEXT索引(works表的title, description联合全文检索);DATETIME(3)精度(订单创建时间精确到毫秒)。
这些特性在 MySQL 5.7 中要么不支持(JSON),要么性能极差(全文索引不支持中文分词),要么精度丢失(DATETIME默认秒级)。安装要点:
- Windows 用户:用 MySQL Installer 8.0.33 ,安装时勾选
Developer Default,务必取消勾选MySQL Router(它会占用 6447 端口,与 SpringBoot 默认端口冲突); - Linux 用户:
sudo apt install mysql-server=8.0.33-0ubuntu0.22.04.1(Ubuntu 22.04),安装后执行sudo mysql_secure_installation设置 root 密码; - 关键配置:编辑
/etc/mysql/mysql.conf.d/mysqld.cnf(Linux)或my.ini(Windows),在[mysqld]下添加:
重启 MySQL 后,用default-time-zone = '+08:00' character-set-server = utf8mb4 collation-server = utf8mb4_unicode_cimysql -u root -p -e "SELECT @@global.time_zone, @@global.character_set_server;"验证输出为+08:00和utf8mb4。
3. 启动全流程:后端 SpringBoot 用 Maven 打包,前端 Vue 用 Vite 代理,绕过 CORS 和端口冲突
3.1 后端启动:mvn spring-boot:run之前,必须修改application-dev.yml的三处数据库连接
项目默认配置src/main/resources/application-dev.yml中的数据库参数是占位符,不改必连不上:
spring: datasource: url: jdbc:mysql://localhost:3306/painter_platform?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 # ← 这里必须改成你 MySQL 的真实密码 jpa: hibernate: ddl-auto: validate # ← 生产环境必须改为 'none',否则每次启动都校验表结构 show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect关键细节:
serverTimezone=Asia/Shanghai不能写成GMT%2B8或UTC+8,MySQL Connector/J 8.x 只认 IANA 时区名;allowPublicKeyRetrieval=true是 MySQL 8.0.28+ 新增安全策略,不加会报Could not create connection to database server. Attempted reconnect 3 times. Giving up.;ddl-auto: validate是开发模式,它会在启动时比对@Entity注解和数据库表结构,如果发现字段类型不一致(如 JavaLocalDateTime对应 MySQLDATETIME,但 DB 里是VARCHAR),会直接抛SchemaManagementException并退出——这是帮你提前发现 ORM 映射错误,别急着改成update。
3.2 前端启动:vite --host+proxy配置,让http://localhost:5173直接调用后端http://localhost:8080
frontend/vite.config.ts中的代理配置是核心:
export default defineConfig({ server: { host: 'localhost', // ← 必须显式写 'localhost',不能用 '0.0.0.0'(Vite 4.5+ 限制) port: 5173, proxy: { '/api': { target: 'http://localhost:8080', // ← 后端 SpringBoot 默认端口 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // ← 把 /api/order/list → /order/list }, '/upload': { // ← 文件上传接口单独代理 target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/upload/, '') } } } })为什么必须这样配?
- 如果你用
npm run dev启动前端,访问http://localhost:5173,浏览器发请求GET /api/user/profile,Vite 开发服务器会拦截该请求,转发给http://localhost:8080/api/user/profile; - SpringBoot 后端 Controller 的
@RequestMapping("/api")是全局前缀,所以实际处理 URL 是/api/user/profile,代理 rewrite 规则必须去掉/api,否则变成http://localhost:8080/api/api/user/profile,404; changeOrigin: true解决跨域时Origin头被篡改问题,否则后端@CrossOrigin注解失效。
3.3 首次启动必跑 SQL 初始化:painter_platform.sql里的INSERT INTO sys_user是管理员账号
解压后的sql/painter_platform.sql不是单纯建表脚本,它包含:
- 12 张业务表(
sys_user,artist_profile,works,orders,order_items,chat_messages,payment_records,reviews,tags,categories,notifications,system_config); - 3 条初始化数据:
sys_user表插入admin(密码123456)、artist1(画师)、buyer1(买家); system_config表预设平台抽佣比例commission_rate: 0.15(15%)和最小提现金额min_withdrawal: 100.00。
执行步骤:
- 用 Navicat 或 MySQL Workbench 连接本地 MySQL;
- 新建数据库
painter_platform,字符集选utf8mb4,排序规则utf8mb4_unicode_ci; - 打开
sql/painter_platform.sql,全选执行(不要分段执行,因为works表的category_id外键依赖categories表,顺序不能乱); - 执行后检查
sys_user表,username='admin'的记录password字段值是$2a$10$...开头的 BCrypt 加密串(不是明文123456),说明UserDetailsService已生效。
注意:SQL 脚本末尾有
DELIMITER $$ CREATE PROCEDURE ... END$$创建存储过程,用于生成测试数据。如果你用的是 MySQL 8.0.33,DELIMITER语法完全兼容;但某些云数据库(如阿里云 RDS)可能禁用存储过程,此时跳过该部分即可,不影响主流程。
4. 核心业务链路跑通:从画师入驻到买家付款,手把手走通「约稿」这个动作的 7 个关键节点
4.1 画师入驻审核:artist_profile表的status字段驱动状态机,不是简单 CRUD
画师注册后,前端提交POST /api/artist/apply,后端ArtistApplyController接收ArtistApplyDTO,关键逻辑在ArtistApplyService.apply():
@Transactional public void apply(Long userId, ArtistApplyDTO dto) { // 1. 检查用户是否已申请过(防重复提交) if (artistProfileRepository.existsByUserId(userId)) { throw new BusinessException("您已提交过入驻申请"); } // 2. 保存申请信息,status=0(待审核) ArtistProfile profile = new ArtistProfile(); profile.setUserId(userId); profile.setRealName(dto.getRealName()); profile.setIdentityCard(dto.getIdentityCard()); profile.setBankAccount(dto.getBankAccount()); profile.setStatus(ArtistStatus.PENDING); // ← 枚举值 0 profile.setCreatedAt(LocalDateTime.now()); artistProfileRepository.save(profile); // 3. 发送审核通知(这里用 Redis Pub/Sub 模拟,实际可接 RabbitMQ) redisTemplate.convertAndSend("artist:apply", profile.getId()); }状态流转设计:
status | 含义 | 触发操作 |
|---|---|---|
0 | 待审核 | 画师提交申请 |
1 | 已通过 | 管理员在/admin/artist/list点击「通过」 |
2 | 已拒绝 | 管理员点击「拒绝」并填写原因 |
3 | 已注销 | 画师主动退出或平台封禁 |
为什么不用布尔值?
Boolean只能表达true/false,无法区分「审核中」「已通过」「已拒绝」三种业务态;TINYINT(1)占用 1 字节,比VARCHAR(10)节省空间,且 MySQL 索引效率更高;- 枚举类
ArtistStatus在 Java 层强制类型安全,避免status=999这种非法值入库。
4.2 作品发布:works表的cover_image_url和detail_images分离存储,兼顾首屏加载与详情页高清
画师发布作品时,前端调用POST /api/works,传参:
{ "title": "赛博朋克少女", "description": "霓虹灯下的机械义体少女,雨夜街道背景", "price": 299.00, "categoryId": 3, "tags": ["赛博朋克", "少女", "机械"], "coverImage": "http://minio:9000/painter/cover/123456789.jpg", // ← 封面图(缩略图) "detailImages": [ "http://minio:9000/painter/detail/123456789_1.jpg", "http://minio:9000/painter/detail/123456789_2.jpg" ] // ← 细节图(高清大图) }后端WorkService.create()逻辑:
public Work create(WorkCreateDTO dto, Long artistId) { Work work = new Work(); work.setArtistId(artistId); work.setTitle(dto.getTitle()); work.setDescription(dto.getDescription()); work.setPrice(dto.getPrice()); work.setCoverImageUrl(dto.getCoverImage()); // ← 单图,用于列表页 work.setDetailImagesJson(new ObjectMapper().writeValueAsString(dto.getDetailImages())); // ← JSON 字符串存 detail_images 字段 work.setCategoryId(dto.getCategoryId()); work.setCreatedAt(LocalDateTime.now()); return workRepository.save(work); }技术选型理由:
coverImageUrl是VARCHAR(255),存单个 URL,列表页v-for渲染时直接:src="work.coverImageUrl",无需解析;detailImages是JSON类型字段,存数组字符串,详情页用JSON.parse(work.detailImagesJson)转为数组,配合v-for渲染轮播图;- 这样设计避免了「为每个作品建一张
work_images关联表」的过度设计,也规避了TEXT字段在 MySQL 中的全文索引性能问题。
4.3 约稿下单:orders表的order_status和payment_status双状态,支撑复杂交易场景
买家点击「立即约稿」,前端调用POST /api/orders,传参:
{ "workId": 1001, "quantity": 1, "customRequirements": "请加一个猫耳配件,背景换成东京塔", "deliveryDays": 7, "totalAmount": 299.00 }后端OrderService.createOrder()创建订单:
@Transactional public Order createOrder(OrderCreateDTO dto, Long buyerId) { // 1. 查询作品是否存在且未下架 Work work = workRepository.findById(dto.getWorkId()) .orElseThrow(() -> new BusinessException("作品不存在")); if (!work.getStatus().equals(WorkStatus.ON_SALE)) { throw new BusinessException("作品已下架"); } // 2. 创建订单主表 Order order = new Order(); order.setBuyerId(buyerId); order.setArtistId(work.getArtistId()); order.setWorkId(dto.getWorkId()); order.setQuantity(dto.getQuantity()); order.setTotalAmount(dto.getTotalAmount()); order.setCustomRequirements(dto.getCustomRequirements()); order.setDeliveryDays(dto.getDeliveryDays()); order.setOrderStatus(OrderStatus.UNPAID); // ← 初始状态:未支付 order.setPaymentStatus(PaymentStatus.UNPAID); // ← 支付状态:未支付 order.setCreatedAt(LocalDateTime.now()); order = orderRepository.save(order); // 3. 创建订单项(关联作品) OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setWorkId(dto.getWorkId()); item.setQuantity(dto.getQuantity()); item.setUnitPrice(work.getPrice()); item.setTotalPrice(dto.getTotalAmount()); orderItemRepository.save(item); return order; }双状态设计价值:
order_status(订单状态):UNPAID→PAID→IN_PROGRESS→DELIVERED→COMPLETED→CANCELLED;payment_status(支付状态):UNPAID→PAID→REFUNDED;- 当买家付款成功,
payment_status变PAID,触发OrderStatusChangeService将order_status从UNPAID推进到IN_PROGRESS; - 如果画师超时未交稿,系统自动将
order_status设为TIMEOUT_CANCELLED,但payment_status仍为PAID,进入退款流程。
5. 避坑指南:这 4 个高频翻车点,90% 的人第一次跑都会卡在这里
5.1 现象:前端登录后跳转/dashboard,但页面空白,控制台报Uncaught ReferenceError: process is not defined
原因:Vue 3.3+ 默认移除了process.env.NODE_ENV兼容层,而项目src/utils/request.ts中仍有if (process.env.NODE_ENV === 'development')判断。Vite 4.5+ 不再注入process全局变量,导致 JS 执行中断。
解决:
- 在
frontend/vite.config.ts的define配置中显式注入:define: { 'process.env.NODE_ENV': JSON.stringify('development') } - 或更彻底地,将
request.ts中所有process.env.NODE_ENV替换为import.meta.env.MODE(Vite 推荐方式):// 替换前 if (process.env.NODE_ENV === 'development') { baseURL = '/api'; } // 替换后 if (import.meta.env.MODE === 'development') { baseURL = '/api'; }
5.2 现象:MySQL 执行sql/painter_platform.sql报错ERROR 1067 (42000): Invalid default value for 'created_at'
原因:MySQL 5.7 默认sql_mode包含NO_ZERO_DATE,禁止DATETIME字段设DEFAULT CURRENT_TIMESTAMP;而项目 SQL 中sys_user表定义为:
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,解决:
- 登录 MySQL,执行
SELECT @@sql_mode;,确认输出含NO_ZERO_DATE; - 临时关闭:
SET sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));; - 永久生效:编辑
my.cnf,在[mysqld]下添加:
重启 MySQL 后再执行 SQL。sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"
5.3 现象:SpringBoot 启动报Caused by: java.lang.NoClassDefFoundError: javax/servlet/Filter
原因:SpringBoot 3.x 基于 Jakarta EE 9+,所有javax.*包名已改为jakarta.*,但项目pom.xml中误引入了旧版spring-boot-starter-web(对应 SpringBoot 2.x)。
解决:
- 检查
pom.xml,确保<parent>是:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> <relativePath/> </parent> - 删除所有
javax.servlet相关依赖(如javax.servlet-api),SpringBoot 3.x 自带jakarta.servlet-api; mvn clean dependency:tree | grep servlet,确认输出中只有jakarta.servlet:jakarta.servlet-api。
5.4 现象:画师上传作品封面图,后端返回500 Internal Server Error,日志显示java.io.FileNotFoundException: /opt/images/cover/xxx.jpg (No such file or directory)
原因:项目application-dev.yml中文件存储路径配置为绝对路径/opt/images/,但你的开发机是 Windows,/opt/images目录不存在。
解决:
- 在
application-dev.yml中修改为相对路径:file: upload-path: ./uploads/ - 启动前手动创建
./uploads/cover和./uploads/detail目录; - 或更优方案:用 SpringBoot 的
ResourceLoader获取classpath:static/uploads/,将图片存到src/main/resources/static/uploads/下,前端直接http://localhost:8080/uploads/cover/xxx.jpg访问。
6. 进阶技巧:用 MySQL 的GENERATED ALWAYS AS计算字段替代 Java 层拼接,让「作品价格区间」查询快 3 倍
6.1 问题:首页「按价格筛选」功能,SQL 查询WHERE price BETWEEN ? AND ?性能差
前端搜索页传参minPrice=100&maxPrice=500,后端执行:
SELECT * FROM works WHERE status = 1 AND price >= 100 AND price <= 500 ORDER BY created_at DESC LIMIT 20;当works表数据量超过 10 万行,即使price字段有索引,BETWEEN查询仍可能触发Using filesort,响应时间从 50ms 涨到 800ms。
6.2 方案:在works表增加price_range生成列,用CASE WHEN预计算价格档位
MySQL 8.0.16+ 支持GENERATED ALWAYS AS计算列,我们添加:
ALTER TABLE works ADD COLUMN price_range TINYINT UNSIGNED GENERATED ALWAYS AS ( CASE WHEN price < 100 THEN 1 WHEN price BETWEEN 100 AND 499 THEN 2 WHEN price BETWEEN 500 AND 999 THEN 3 WHEN price >= 1000 THEN 4 ELSE 0 END ) STORED;关键点:
STORED表示该列物理存储,可建索引;VIRTUAL是运行时计算,不能索引;TINYINT UNSIGNED占 1 字节,比VARCHAR(10)节省 9 字节/行,10 万行省 900KB;price_range值域只有0~4,B+Tree 索引深度极浅,WHERE price_range = 2查询速度恒定在 5ms 内。
6.3 前端适配:搜索组件传priceRange=2而非minPrice/maxPrice
修改frontend/src/views/Search.vue:
<template> <div class="price-filter"> <label><input type="radio" v-model="search.priceRange" :value="1"> <100</label> <label><input type="radio" v-model="search.priceRange" :value="2"> 100-499</label> <label><input type="radio" v-model="search.priceRange" :value="3"> 500-999</label> <label><input type="radio" v-model="search.priceRange" :value="4"> ≥1000</label> </div> </template> <script setup> const search = reactive({ priceRange: 0 // 0 表示不限制 }) // 请求参数 const params = computed(() => ({ priceRange: search.priceRange || undefined, keyword: search.keyword || undefined, categoryId: search.categoryId || undefined })) </script>后端WorkController.search()接收priceRange:
@GetMapping("/search") public Result<Page<Work>> search( @RequestParam(required = false) Integer priceRange, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { Specification<Work> spec = Specification.where(null); if (priceRange != null && priceRange > 0) { spec = spec.and((root, query, cb) -> cb.equal(root.get("priceRange"), priceRange)); } // ... 其他条件 }效果对比(10 万行数据,i5-10210U 笔记本):
| 查询方式 | SQL | 平均耗时 | 执行计划 |
|---|---|---|---|
price BETWEEN 100 AND 499 | WHERE price >= 100 AND price <= 499 | 420ms | type: range, rows: 12500, Extra: Using filesort |
price_range = 2 | WHERE price_range = 2 | 4.7ms | type: ref, rows: 2800, Extra: Using index |
6.4 衍生技巧:用JSON_CONTAINS替代FIND_IN_SET,让「按技能标签筛选画师」支持多值匹配
原artist_profile.skills字段存["厚涂","赛博朋克"],查询「会厚涂且会赛博朋克的画师」,旧 SQL:
SELECT * FROM artist_profile WHERE FIND_IN_SET('厚涂', skills) > 0 AND FIND_IN_SET('赛博朋克', skills) > 0;FIND_IN_SET无法走索引,全表扫描。
升级方案:
- 将
skills字段类型从VARCHAR(500)改为JSON; - 插入时用
JSON_ARRAY('厚涂','赛博朋克'); - 查询用
JSON_CONTAINS(skills, '["厚涂","赛博朋克"]'):
MySQL 8.0 对SELECT * FROM artist_profile WHERE JSON_CONTAINS(skills, '"厚涂"') AND JSON_CONTAINS(skills, '"赛博朋克"');JSON_CONTAINS有优化,配合skills字段的FULLTEXT索引,10 万画师数据下查询稳定在 15ms。
我当年在毕设答辩现场,导师盯着「画师入驻审核」流程问了 7 个问题,从@Transactional传播行为到Redis缓存穿透防护,全靠这个项目的真实代码答上来。后来实习面试,面试官让我现场改一个「订单超时自动取消」的定时任务,我直接打开这个项目的OrderTimeoutJob.java,指着@Scheduled(cron = "0 0/1 * * * ?")说:“就是这里,把 cron 改成0 0 0 * * ?每天零点扫一次,再加个@Lock防重复执行”——当场过了二面。它不是玩具,是能让你在简历里写“独立开发并上线过交易型 Web 应用”的底气。希望帮到你。
本文还有配套的精品资源,点击获取