资讯动态

基于Vue与Spring Boot的校园二手交易平台开发全解析

发布时间:2026/9/14 14:23:01 来源:尧图企业网站定制
简介这是一份基于Java的校园二手交易平台毕业设计项目采用Vue前端与SpringBoot后端组合实现面向计算机专业正在准备毕业设计、课程作业或项目实战练习的学生。项目曾获导师认可并取得评审98分源码经本地编译调试可运行同时附带数据库文件能直接用于功能演示、二次开发或论文辅助说明。压缩包共363个文件主要包含113个Java源码、56个class编译文件、18个Vue页面、32个XML配置以及9个SQL数据库脚本等整体13.44MB目录清晰便于按前后端模块查看。已有171人学习下载。资料还覆盖订单、闲置物品、用户等核心模块的实现逻辑适合希望理解校园二手交易平台完整业务闭环、需要可运行毕业设计参考源码的学习者。1. 校园二手交易平台选型为什么毕设总绕不开 vue 加 springboot每年毕业设计选题清单里校园二手交易平台几乎都是固定嘉宾。原因不复杂它业务边界足够清晰——用户注册登录、商品发布、商品浏览、留言咨询、订单交易五个模块就能拼出一个完整闭环。前端能看到页面交互后端能看到接口设计数据库能看到表关系设计再加一个管理员后台工作量可多可少难度可深可浅天然适合本科阶段展示全栈能力。但真正让这个题目经久不衰的是技术栈的选择余地用 JSP 加 Servlet 做太老旧用 Python 加 Flask 做又显得没挑战而 Vue 加 Spring Boot 恰好是近五年企业招聘里出现频率最高的前后端分离组合之一做出来直接能写进简历。这里说的源码加数据库落到实际交付形态通常是一个 Spring Boot 后端工程一个 Vue 前端工程一个 MySQL 初始化脚本外加一份部署说明文档。后端负责提供 REST 接口和业务逻辑前端负责页面渲染和交互数据库负责持久化。你不需要在论文里吹嘘多高深的技术只需要把每一个模块的来龙去脉讲清楚把接口设计的合理性说明白就能拿到一个不错的分数。本文会从工程骨架开始逐步带你过一遍后端接口、前端页面、数据库表设计最后补上答辩前必须验证的那几个关键点。2. Spring Boot 后端工程骨架与核心接口实现2.1 工程初始化和依赖选择的三个思路创建 Spring Boot 工程的方式很多最常见的是用 IDEA 的 Spring Initializr 直接生成也可以用 Spring 官方脚手架网站下载压缩包。无论哪种方式核心依赖就那么几个spring-boot-starter-web 提供 MVC 能力mybatis-plus 或 spring-boot-starter-data-jpa 负责数据库访问mysql-connector-j 是驱动lombok 减少样板代码。如果做登录鉴权spring-boot-starter-security 加上 jjwt 够用不想引入 Security 的复杂度也可以自己写拦截器加 JWT 工具类。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这段依赖配置里mybatis-plus 是国产 ORM 框架相比 JPA 它的学习曲线更平缓分页插件、条件构造器都是开箱即用做毕设时能省不少 SQL 拼接的工夫。把 scope 标成 runtime 的 mysql 驱动意味着它只在运行期被加载编译期不需要引用。如果你所在学校要求必须用 JPA那就把 mybatis-plus 替换成 spring-boot-starter-data-jpa写法上差异不大但本篇后续代码以 MyBatis-Plus 为例展开。工程目录建议按 controller、service、mapper、entity、config、common 分层。不要图省事把所有类堆在根包下面答辩时老师翻到乱七八糟的包结构会扣印象分。配置文件里要写明端口、数据库连接、日志级别和 mybatis-plus 的驼峰映射配置这是新手最容易漏掉的一环。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_market?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里要特别注意 URL 里的 serverTimezone 参数MySQL 8.x 对时区敏感不写这个参数启动时会直接报错。map-underscore-to-camel-case开启后数据库字段user_name能自动映射到实体的userName省掉一堆TableField注解。log-impl配置成 StdOutImpl可以在控制台直接看到每个 SQL 的执行情况排查问题比盲猜快得多。2.2 用户与商品实体的设计实体类直接映射数据库表字段类型要和数据库列类型对应。用户表通常包含用户名、密码、昵称、学号、手机号、头像地址、角色、创建时间。密码不能存明文至少用 BCrypt 加密Spring Security 自带 BCryptPasswordEncoder不引入 Security 也能单独把 spring-security-crypto 这个包拎出来用。商品表则要包含发布者 ID、标题、描述、价格、图片 URL、分类、成色、交易状态、发布时间。Data TableName(tb_user) public class User { TableId(type IdType.AUTO) private Integer id; private String username; private String password; private String nickname; private String studentNo; private String phone; private String avatar; private Integer role; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; }TableName注解用来指定实体对应的数据表名TableId声明主键并指定自增策略TableField(fill FieldFill.INSERT)配合 MetaObjectHandler 可以在插入时自动填充创建时间不用每次手动 set。毕设场景下用 Lombok 的Data生成 getter/setter 完全没有问题但答辩老师如果问起你要能说清楚 Lombok 是编译期注解处理器生成的代码在编译后才会出现在 class 文件中。2.3 商品发布和分页查询的 Controller 写法后端接口是前后端分离架构的中间层RESTful 风格是默认要求。商品模块一般要有发布商品、分页查询、按 ID 查详情、修改商品状态、根据用户 ID 查我发布的商品这几个接口。分页查询用 MyBatis-Plus 的 Page 对象拼条件构造器前端传页码和每页条数后端返回总记录数和列表数据。RestController RequestMapping(/api/goods) public class GoodsController { Autowired private GoodsService goodsService; PostMapping public Result publish(RequestBody Goods goods) { goods.setStatus(0); goods.setCreateTime(LocalDateTime.now()); goodsService.save(goods); return Result.success(goods.getId()); } GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Integer categoryId) { LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(keyword), Goods::getTitle, keyword) .eq(categoryId ! null, Goods::getCategoryId, categoryId) .eq(Goods::getStatus, 0) .orderByDesc(Goods::getCreateTime); PageGoods page goodsService.page(new Page(pageNum, pageSize), wrapper); return Result.success(page); } }这段代码里有几个细节值得在答辩时主动讲。RequestParam(defaultValue 1)意味着前端不传页码时默认取第一页防止空指针LambdaQueryWrapper的.eq(condition, column, value)第一个参数是布尔条件条件为 false 时这一条查询条件自动跳过这样关键字为空时不会拼出WHERE title 这种错误查询。发布商品时服务端强制把 status 置为 0表示在售不允许客户端传入一个初始状态这是状态机原则在接口层的体现。Result 是统一返回体里面通常有 code、msg、data 三个字段前端靠 code 判断请求是否成功业界一般约定 200 成功、500 失败。2.4 登录鉴权JWT 拦截器的踩坑点毕设登录模块最常见的方案是登录接口验证用户名密码成功后签发一个 JWT前端把它存在 localStorage之后每次请求都在请求头里带Authorization: Bearer token后端写一个拦截器统一校验。用 Spring Security 做这件事配置繁琐对新手不友好自己写拦截器加 HandlerInterceptor 则是十几行代码的事但有几个坑必须避开。Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String secret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }这段代码第一个坑在 OPTIONS 请求。前端跨域时浏览器会先发一个预检请求方法就是 OPTIONS它不带业务信息如果拦截器拦它前端永远进不了真实接口必须在拦截器里直接放行。第二个坑是跨域配置Spring Boot 里用CrossOrigin注解或者 CorsFilter 都能解决但要确认允许的请求头里包含 Authorization。最后一个坑是异常处理JWT 签名不对、过期、格式错误都会抛不同异常必须 catch 后统一返回 401否则异常抛到全局处理器里会被包装成 500前端没法区分是登录过期还是服务器错误。3. Vue 前端页面搭建与接口联调的细节3.1 工程初始化和路由划分Vue 前端工程用官方脚手架 Vite 创建是最省事的。npm create vitelatest之后选择 Vue 3 模板再安装 vue-router 和 pinia 就够了UI 组件库推荐 Element Plus表格、表单、弹窗都是现成的能省大量样式时间。Vite 的配置文件里要加一个开发代理把/api开头的请求转发到后端 8080 端口解决开发环境跨域问题。// vite.config.js export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })代理配置里changeOrigin: true的作用是修改请求头中的 Host 字段为目标地址的 Host后端按域名或端口做校验时不会误判。路由设计建议按照页面功能划分首页、商品列表、商品详情、发布商品、个人中心、登录注册、管理员后台使用懒加载方式引入组件打包时每个页面拆成独立 chunk首屏加载更快答辩演示刷新页面时也不会白屏。3.2 Axios 封装与登录态注入每个接口都在组件里直接调 axios 的后果是token 注入逻辑重复、错误处理散落各处、接口地址一旦变化要全局搜索替换。成熟的思路是封装一个请求模块统一设置 baseURL、超时时间、请求拦截器、响应拦截器。请求拦截器从 localStorage 取 token 放进请求头响应拦截器里统一处理 code 非 200 的情况和 HTTP 401 状态码。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这里超时时间设 10 秒是兼顾正常接口和图片上传时的等待体验图片压缩上传在弱网环境下可能超过这个阈值可以根据需求放宽到 30 秒。响应拦截器在 401 时清 token 并跳转登录页这是会话过期自动重登的标准写法。注意拦截器里用了 ElMessage 做全局提示Element Plus 是按需引入时这里需要单独导入样式文件否则提示气泡没有样式。3.3 商品列表页的渲染与服务端分页对齐商品列表页是平台的门面需要展示商品缩略图、标题、价格、成色标签还要支持按分类筛选和关键字搜索。Element Plus 的 el-table 适合后台管理场景C 端页面用 el-card 网格布局视觉上更自然。分页组件用 el-pagination它的 current-page 和 page-size 要和后端接口参数严格对应。template div classgoods-list el-row :gutter16 el-col :span6 v-foritem in list :keyitem.id el-card clickgoDetail(item.id) classgoods-card img :srcitem.imageUrl classgoods-img / div classgoods-title{{ item.title }}/div div classgoods-price¥{{ item.price }}/div /el-card /el-col /el-row el-pagination layoutprev, pager, next :totaltotal :current-pagequery.pageNum :page-sizequery.pageSize current-changehandlePageChange / /div /template script setup import { ref, reactive, onMounted } from vue import request from ../utils/request const list ref([]) const total ref(0) const query reactive({ pageNum: 1, pageSize: 8, categoryId: null, keyword: }) const fetchList async () { const res await request.get(/goods/page, { params: query }) list.value res.data.records total.value res.data.total } const handlePageChange (page) { query.pageNum page fetchList() } onMounted(fetchList) /scriptel-col 的:span6表示每行显示 4 列24 栅格除以 6在笔记本屏幕上刚好不拥挤。分页切换时只改变 pageNum 然后重新请求接口这是典型的前端服务端分页不同于一次性把所有数据拉到前端再分页数据量大时不会卡顿。后端返回的 Page 对象里包含 records、total、current、size 等字段前端只用 records 和 totalother 字段留给需要做复杂分页控制的场景。这里有个小细节el-pagination的current-change事件回调参数就是页数不用再从事件对象里取。3.4 发布商品表单与图片上传的格式陷阱发布商品页面涉及的字段比列表页多一截标题、描述、价格、分类、成色、图片。价格字段用 el-input-number 控制能避免用户在文本框里输入非数字内容。图片上传用 el-upload 组件关键是action地址要指向后端上传接口headers里带上 tokenon-success回调里拿到后端返回的图片 URL 后存到表单字段里而不是直接提交文件对象。el-form :modelform :rulesrules refformRef label-width80px el-form-item label商品标题 proptitle el-input v-modelform.title placeholder请输入标题 maxlength50 show-word-limit / /el-form-item el-form-item label价格 propprice el-input-number v-modelform.price :min0 :precision2 :step1 / /el-form-item el-form-item label图片 el-upload action/api/upload :headers{ Authorization: Bearer localStorage.getItem(token) } :on-successhandleUploadSuccess list-typepicture-card :limit5 i classel-icon-plus/i /el-upload /el-form-item /el-form图片上传接口通常由后端实现接收 multipart 文件后存到服务端的 upload 目录或对象存储返回一个可访问的 URL。handleUploadSuccess里要把 res.data 即图片 URL 推入一个数组字段提交商品数据时后端拿到的就是这个数组。el-upload 默认会把文件对象作为 multipart 表单字段file发送后端用RequestParam(file) MultipartFile file接收。注意 el-input-number 的precision2表示保留两位小数但它的值类型是 Number不是字符串后端用 BigDecimal 接收时要做好类型转换避免出现精度丢失的问题。4. 数据库表设计从 ER 模型到落库的关键取舍4.1 核心表结构与字段设计校园二手交易平台的数据库表不必太多五到六张就能撑起全部业务用户表、商品表、分类表、订单表、留言表再加一张轮播图表做首页展示。商品表是核心表字段要把商品的展示属性和交易属性区分开——展示属性如标题、图片、描述、成色偏向 C 端展示交易属性如价格、状态、买家 ID 偏向流程控制。CREATE TABLE tb_goods ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL COMMENT 发布者ID, category_id int DEFAULT NULL COMMENT 分类ID, title varchar(100) NOT NULL COMMENT 商品标题, description text COMMENT 商品描述, price decimal(10,2) NOT NULL COMMENT 价格, image_url varchar(500) DEFAULT NULL COMMENT 主图URL, condition_level tinyint DEFAULT 1 COMMENT 成色1全新 2几乎全新 3轻微使用 4明显磨损, status tinyint DEFAULT 0 COMMENT 状态0在售 1已预约 2已售出 3下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;decimal(10,2)表示最多 8 位整数加 2 位小数单价精度完全够用比 float/double 更精确不会出现 9.99 存成 9.9900001 的尴尬。condition_level用 tinyint 存数字而不是直接存字符串好处是后端可以定义一个枚举类做映射减少存储冗余。create_time用DEFAULT CURRENT_TIMESTAMP让数据库自动填充update_time加ON UPDATE CURRENT_TIMESTAMP则会在记录更新时自动刷新这两个默认行为能省掉后端代码里大量手写时间戳的重复工作。索引设计上idx_user_id支撑我的发布查询idx_status_time支撑按状态和发布时间排序的列表页二者覆盖了访问频率最高的两条查询路径。分类表非常简单只有 id、name、sort 三个字段订单表则需要多花心思它关联买家、卖家和商品字段上除了三个外键还要有订单号、金额、状态、创建时间、完成时间。订单号不要用自增 ID 直接暴露给前端用时间戳加随机数生成一个业务订单号更能保护业务数据。4.2 商品状态流转与订单表的事务控制商品状态有在售、已预约、已售出、下架四种但用户端能看到的只有前三种。下单动作涉及两张表的修改订单表插入一条记录、商品表把状态改成已售出。这两个操作必须放在同一个事务里否则会出现订单生成了但商品还在售的数据不一致。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { Goods goods goodsMapper.selectById(dto.getGoodsId()); if (goods null || goods.getStatus() ! 0) { throw new BusinessException(商品不存在或已被购买); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setGoodsId(goods.getId()); order.setSellerId(goods.getUserId()); order.setBuyerId(dto.getBuyerId()); order.setAmount(goods.getPrice()); order.setStatus(0); orderMapper.insert(order); goods.setStatus(2); goodsMapper.updateById(goods); return order.getId(); }这段代码有几个必须掌握的点。第一Transactional注解默认只在抛出 RuntimeException 时回滚rollbackFor Exception.class把受检异常也纳入回滚范围。第二查询商品状态要在事务内进行并且最好加上SELECT ... FOR UPDATE行锁否则两个用户同时下单同一件商品时两个请求都读到 status0都执行了后面的插入和更新就会出现超卖。MyBatis-Plus 里可以用selectForUpdate或者自定义 SQL 实现。第三先插订单后改商品顺序不能反因为商品状态变更失败时订单已经插入回滚机制能处理但如果先改商品状态再插订单订单主键生成失败时商品状态被回滚也没问题只是日志排查时顺序不直观。统一先加订单后改状态的顺序配合事务回滚逻辑上更清晰。4.3 初始化数据与演示账号的准备数据库脚本除了建表语句还要包含必要的初始化数据。分类表的预置数据是必须的比如数码、图书、生活用品、运动器材、其他分类维度不需要太细5 到 8 个即可太细反而让用户发布时选择困难。演示账号要准备两个角色的用户一个普通学生账号、一个管理员账号密码统一用 BCrypt 加密后的密文写在 SQL 里。INSERT INTO tb_user (username, password, nickname, student_no, role) VALUES (student, $2a$10$E4k0zYKQvXTsVgHbA0Xc2u7yW1QZzt7pKQa1AiwGYqJmJXZKkWyNK, 测试学生, 20200001, 0), (admin, $2a$10$E4k0zYKQvXTsVgHbA0Xc2u7yW1QZzt7pKQa1AiwGYqJmJXZKkWyNK, 管理员, 20200002, 1);密文是 BCrypt 加密后的结果直接复制到一个能生成 BCrypt 哈希的工具里生成。用同一个密文创建两个账号在演示场景下没问题但正式答辩时最好每个账号单独生成密文体现你对安全性的重视。另外在商品表里预置 10 条左右的商品数据覆盖不同分类和不同状态这样演示时打开首页就有内容可看不用现场发布。数据库脚本文件建议拆成两个schema.sql 建表、data.sql 初始化数据文档里写清楚导入顺序复制粘贴到 Navicat 里执行时不容易出错。4.4 索引设计误区和查询性能自查毕设答辩时评委喜欢问索引但问的往往不深。你需要掌握的是主键索引是聚簇索引其他索引是二级索引二级索引查到主键后还要回表。商品列表页查询条件是 status 加 create_time所以建了idx_status_time(status, create_time)这是等值条件加排序场景最典型的最左前缀索引设计。如果再加一个 category_id 的等值条件这个索引就不完全匹配了MySQL 会先按索引查到 status 匹配的数据再过滤 category_id数据量不大时性能没区别但你要能说出这个逻辑。常见的索引误区给所有字段都加索引、在低区分度字段上建索引比如 status 只有 4 个值、索引字段上做函数运算导致索引失效。自查方法很简单用EXPLAIN SELECT * FROM tb_goods WHERE status 0 ORDER BY create_time DESC LIMIT 10看 type 字段是不是 ref 或 rangekey 字段是不是实际命中了 idx_status_timerows 字段预估扫描行数是否合理。这条语句在答辩时现场演示非常加分。5. 部署演示与答辩讲稿的三处点睛5.1 本地打包与一键启动的验证路径毕业设计验收有两种常见形式现场用自己的电脑演示或者部署到服务器上供评委访问。无论是哪一种都要提前验证从源码到可运行的全流程。后端打包用 Maven 的 package 命令生成 jar前端用 vite build 生成 dist 目录然后需要解决静态资源由谁托管的问题。两个选择把 dist 复制到后端 resources/static 下和后端一起打包成单 jar或者用 Nginx 反向代理前端和后端。论文里写单 jar 方案更稳妥演示时一台电脑一个命令就能跑起来。cd backend mvn clean package -DskipTests java -jar target/campus-market-0.0.1.jar-DskipTests跳过测试用例编译和运行打包速度快不少。前端打包前要确认baseURL是相对路径/api打包后 dist 拷入后端 static 目录。如果接口请求路径写成http://localhost:8080/api这种绝对地址部署到服务器时又要改代码用相对路径配合路由前缀就能通吃所有环境。启动后打开浏览器访问http://localhost:8080能直接看到首页说明前后端部署打通了。别忘了数据库要先导入脚本否则启动时 MyBatis 执行 SQL 会因表不存在直接报错。5.2 答辩演示顺序上的三个加分点答辩演示不建议从注册开始走完整流程评委时间有限看的是关键节点。推荐顺序先演示登录演示完立刻打开浏览器开发者工具的 Network 面板找到登录请求指出请求头中的 Authorization 字段和返回的 JWT 结构——这一下就让评委知道你是真的懂鉴权原理。然后进入商品列表页筛选一个分类在 Network 面板里切换 Fetch/XHR 过滤展示分页参数 pageNum、pageSize 是如何随点击变化的。最后演示发布商品上传一张图片提交后在数据库工具里执行SELECT * FROM tb_goods ORDER BY id DESC LIMIT 1把数据库里的记录和页面上展示的信息对照给评委看。这套顺序把前后端交互、数据库落库都展示到了全程不到五分钟。5.3 评委容易追问的技术点与应答思路评委听完演示通常都会追问几个固定的地方。第一个是为什么选 MyBatis-Plus 而不是 JPA应答思路是从学习成本和 SQL 可控性切入毕设项目表结构简单MyBatis-Plus 的代码生成器能直接生成 CRUD 代码LambdaQueryWrapper 能避免 SQL 注入同时保留了自定义 SQL 的能力。第二个是商品超卖问题怎么解决的应答思路是提乐观锁或悲观锁的概念讲清楚代码里加了行锁和状态校验。如果评委继续追问锁的粒度就主动说你用的是 MySQL 的 InnoDB 行锁配合事务隔离级别在单库单表的场景下足够覆盖需求。第三个是图片存储的问题如实回答存的是本地目录或 OSS 都行关键是后续主动指出图片 URL 的生成规则和静态资源映射配置证明这个模块是你自己实现的。最后一个建议把数据库的 SQL 脚本导出一份带注释的执行日志答辩前在干净的 MySQL 环境里完整跑一遍确保没有任何一个字段名或者外键写错。很多毕设翻车不是因为功能缺失而是配置环境时数据库连不上、端口被占用、前端请求跨域被拦这些低级问题提前十分钟过一遍就能全部避免。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价