ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue全栈实战:二手旧物回收商城系统设计与实现

SpringBoot+Vue全栈实战:二手旧物回收商城系统设计与实现 每年到毕业季总有一批人被同一个问题卡住要技术栈有java、vue、springboot要业务场景还得是能落地、不假大空的东西。如果你正在发愁或者想找个全栈项目练手二手旧物回收商城这个方向确实值得认真做一遍。我这次完整梳理一下从需求、表结构、后端核心模块到前端页面和部署调试的整个实现思路里面每一段都是实际写代码时会遇到的真实问题不只是把系统跑起来那么简单。这个项目的名义是“二手旧物回收商城”本质上要做成两套业务闭环一是二手闲置商品的展示、下单、交易二是旧物回收预约的上门回收流程。两头都打通系统才算完整而不是做一个只挂着“回收”两个字但实际只有商品CRUD的假项目。适合的人群也很明确准备用SpringBootVue做毕设的同学、想练全栈项目经验的初级开发以及想系统了解订单状态机、权限模型、文件上传这些高频面试点的朋友。1. 需求梳理这个二手旧物回收商城到底包含哪些功能1.1 两条核心业务线商品交易与回收预约拿到这种项目题目第一件事不是建SpringBoot工程而是把业务角色和流程画清楚。我见过太多人上来先写实体类写到订单表的时候就开始纠结“买家卖家怎么区分”“回收单和订单要不要放一张表”本质都是需求没理清。这个系统我拆成两条线来看商品交易线用户A发布一件闲置二手商品管理员审核通过后上架用户B浏览、搜索、加入购物车、下单、支付A收到订单后确认发货B确认收货订单完成。如果B不想买了支付前可以取消订单支付后发起退款申请。回收预约线用户有一些旧衣物、旧书、旧家电、手机数码之类的东西不想发布成商品慢慢卖直接填一个回收预约单选择品类、预约时间段、填写地址和物品描述。回收员在后台看到待接单的预约抢单/接单按时间上门当面确认物品后录入实际回收价格用户确认回收完成回收款转入用户余额用户可申请提现。两条线的用户角色都是同一套账号体系普通用户既能在商城买东西也能预约回收旧物还能在个人中心看到自己的商品订单和回收记录。管理员负责商品审核、分类管理、回收单分配、用户管理、公告管理。回收员是第三个独立角色专门处理回收流程。1.2 功能模块清单不该少的和容易被忽略的按上面的业务线拆模块最少需要这些功能用户端注册登录、个人信息维护、收货地址管理、浏览商品、搜索/分类筛选、商品详情、购物车、下单支付、订单列表、订单详情、取消/确认收货、发布闲置商品、回收预约、回收记录、余额与提现、收藏商品、公告查看管理员端登录、用户管理、分类管理、商品审核/上下架、商品管理、订单管理、回收预约管理、回收员管理、公告管理、数据统计回收员端登录、待接单列表、我的回收任务、录入回收价格、完成回收、回收收益记录容易被忽略但有亮点的功能我特意加上了三个商品审核流程、回收员抢单机制、订单超时自动取消。这三个功能在数据库设计和后端逻辑上都能体现技术含量面试时也是很好的展开点。尤其是商品审核很多同学的毕设项目是发布即上架这其实不太合理二手平台必须有审核环节来控制违规商品加上这个流程系统的完整度立刻不一样。1.3 系统角色与权限边界整个系统至少需要三种角色USER普通用户、ADMIN管理员、COLLECTOR回收员。我建议在多用户登录的设计上就直接用角色字段区分而不拆成三套独立的登录表这样体检中心和个人中心逻辑都统一。JWT令牌里存userId和role后端用拦截器校验登录状态再用注解做角色权限控制比如只有ADMIN能调用商品审核接口只有COLLECTOR能接回收单。权限这块很容易被忽略但确实很重要。我在做的时候遇到过一个实际场景商品审核接口忘记加权限校验结果普通用户直接调接口把自己的商品强制上架了。后来我把所有接口按角色过了一遍给管理端接口统一加了RequireRole(ADMIN)注解才算踏实。项目答辩的时候可以重点讲一下这里的设计思路比单纯说“我用了Spring Security”更有说服力。2. 技术选型为什么是SpringBootVue这套组合以及对应的替代方案2.1 后端选型的理由后端用SpringBoot几乎是这类项目的标准答案。原因很直接SpringBoot帮我们把Spring的配置自动化内嵌Tomcat不需要额外部署WAR包开发阶段一个main方法就能启服务。对于我这种要快速实现项目的场景省下的时间可以全花在业务逻辑上。具体技术栈我选的是Spring Boot 2.7.x / 3.x都行关键是JDK版本匹配。3.x要求JDK17如果本机是JDK8就老老实实用2.7。MyBatis Plus单表CRUD可以完全不写SQL分页插件、逻辑删除、自动填充这些功能都是现成的比纯MyBatis省太多事。MySQL 8.x存业务数据字符集统一utf8mb4。Redis做验证码缓存、Token黑名单、首页轮播图缓存。这一个就能在项目亮点里占一条。JWT无状态登录方案用户登录后生成token前端每次请求带在Header里。Lombok省掉getter/setter实体类清爽很多。Hutool生成验证码、ID生成这些工具类非常方便。为什么不用Spring Security真要硬塞也能塞进去但学习成本和配置成本都会高不少。对这种商城项目用拦截器做登录校验、自己写角色注解反而更可控面试的时候被问到底层原理也不心虚。Spring Security可以放在嘴边的“我还了解”这一层没必要为了用而用。2.2 前端选型的理由前端我选的是Vue 3 Vite Element Plus Pinia Axios Vue Router。Vue 3的组合式API写起来逻辑内聚性更好同一个功能的响应式变量和方法放在一起而不是像Vue 2的选项式那样分散在data、methods、computed里。Vite开发服务器启动速度快热更新比webpack时代的体验好太多。状态管理用Pinia而不是Vuex因为Pinia更轻、API更简洁没有mutations那一层的冗余写法而且它对TypeScript的支持更好。UI库用Element Plus表格、表单、弹窗、分页这些管理后台需要的组件都有现成的风格也比较统一。Axios封装一下请求和响应拦截器统一处理token注入、401跳转、错误消息提示这是每个Vue项目都要做的基建工作。2.3 为什么不建议换成其他方案的几个考虑有的同学喜欢用关系型数据库的H2或者SQLite图省事我强烈不建议。MySQL就多一个安装配置的事但对后续的面试解释、代码审查、部署上线都有帮助。H2做毕设演示确实轻便但面试官一问“你的数据最终存在哪”答H2会显得项目很玩具。后端换成Node.js或Python Django当然也能做但问题在于这类毕设题目的关键词已经把技术栈限定成java、vue、springboot了直接用最匹配的方案是最稳妥的。如果换成SSMSpring MVC Spring MyBatis又会陷入一堆XML配置里纯粹自找麻烦。SpringBoot就是把SSM的繁琐配置默认化这也是它能成为主流的原因。前端不用Vue CLI而用Vite是因为Vite基于ES Module的处理方式让冷启动和热更新都更快。不过我提醒一句Vite要求Node.js 14.18你自己电脑上如果是老版本Node记得先升级。还有人用Ant Design Vue也不错但我用Element Plus是因为后台管理场景它组件更全表格和表单的交互更顺手。3. 数据库设计核心表结构、订单状态和回收单状态机3.1 标准库表结构清单这个项目我设计了12张表左右核心几张列出来供参考表名说明关键字段user用户表id, username, password, nickname, avatar, phone, role, balance, statuscategory商品分类表id, name, sort, icon, statusproduct闲置商品表id, user_id, category_id, title, description, cover, images, price, original_price, status, view_countcart购物车表id, user_id, product_id, quantityproduct_order商品订单表id, order_no, user_id, seller_id, product_id, amount, status, address_snapshot, pay_time, ship_time, finish_timeaddress收货地址表id, user_id, receiver_name, receiver_phone, province, city, district, detail, is_defaultrecycle_appointment回收预约表id, appointment_no, user_id, collector_id, category, description, appointment_time, address, status, estimated_amount, actual_amountrecycle_record回收完成记录表id, appointment_id, user_id, collector_id, amount, balance_before, balance_after, create_timewithdraw_record提现记录表id, user_id, amount, account, status, reject_reasonbanner轮播图表id, image, url, sort, statusnotice公告表id, title, content, status, create_timecollection收藏表id, user_id, product_id, create_time有些字段我额外做了冗余设计比如订单表里的seller_id和address_snapshot。地址快照的意思是下单时把买家填的收货地址整段存进订单而不是关联地址表ID因为地址以后可能被修改但订单的收货信息必须保持下单时的样子。这个设计在电商系统里非常重要面试被问“为什么订单表不直接关联地址表”的时候这就是标准答案。商品表里的images字段我建议存JSON数组格式的字符串比如[https://xxx/1.jpg,https://xxx/2.jpg]前端请求详情页时直接JSON.parse就能轮播展示。用独立的商品图片表也能做但查询要多一次联表对这个规模的项目反而没必要。3.2 商品订单状态机设计商品订单状态我设计成六个状态待支付、待发货、待收货、已完成、已取消、退款中。数据库里用int类型存状态码0待支付、1待发货、2待收货、3已完成、4已取消、5退款中代码里定义常量类避免魔法数字。完整流转是这样创建订单状态0待支付同时创建订单项快照用户支付状态0 - 状态1记录支付时间卖家发货状态1 - 状态2记录发货时间二手交易里可以简化成“卖家确认发货”买家确认收货状态2 - 状态3记录完成时间卖家余额增加超时未支付状态0 - 状态4由定时任务统一处理退款申请状态2 - 状态5管理员确认后退款给买家状态回滚并关闭订单状态机最大的价值在于让非法状态跳转无处藏身。我在Service层写了一个OrderStatusTransition工具类每次状态变更前先校验当前状态是否允许跳到目标状态不允许就直接抛业务异常。比如订单在“已完成”状态下又调用“取消”接口就会被拦截不会产生脏数据。3.3 回收单状态机设计回收预约单的状态我也用数字存的0待审核、1待接单、2待上门、3已确认待打款、4已完成、5已取消、6已驳回。流程是用户提交预约 - 管理员/系统审核0 - 1审核不通过直接6- 回收员接单1 - 2- 回收员上门确认物品并录入实际金额2 - 3- 用户确认回收完成钱款打入余额3 - 4- 完成。如果用户临时有事在2之前可以取消状态置为5。这里有个容易踩坑的点回收金额录入后不能马上打到用户余额必须等用户确认才能入账防止回收员乱填价格。我在回收员录入金额后会让系统自动给用户发一条通知用户确认后才调余额变更逻辑并且用事务保证“更新预约单状态”和“增加用户余额”两个操作要么都成功要么都失败。开发时曾出现过用户余额加了但预约单状态没更新的情况后来加了Transactional注解解决了。3.4 Redis在这个项目里的真实用途Redis最实在的三个用途我建议大家一定写上一是图形验证码的存储和校验验证码5分钟过期用Redis过期时间天然实现二是登录Token的“记住我”语义管理虽然JWT本身无状态但退出登录场景下可以把token加入黑名单Redis里存一个key为token、值为1、过期时间和token一致的黑名单三是首页轮播图和热门商品的数据缓存后台修改轮播图时删除缓存下次请求自动回源数据库。有同学问我为什么不用Redis存购物车不是不行但购物车数据在数据库里更好做列表分页和持久化毕设项目没必要把所有东西都塞Redis反而会被追问“Redis宕机了购物车怎么办”这种问题。4. 后端核心实现从登录鉴权到订单状态流转的代码思路4.1 项目目录结构和统一结果集我建议后端包结构按功能模块分不按技术类型硬分。service、mapper、controller、entity、dto、vo、config、common、utils。模块之间用DTO/VO隔离不要直接把Entity返回给前端。统一结果集ResultT是一个必须自己封装的基础类包含code、message、data三个字段。成功返回Result.success(data)失败返回Result.error(参数错误)配合全局异常处理器controller里的代码就能非常干净。比如查询用户信息的方法只需要写业务代码不用每个接口都手动try-catch。4.2 JWT登录与接口权限控制登录流程是这样的用户提交用户名密码校验通过后生成JWTpayload里放userId和rolesecret密钥放在配置文件中响应给前端前端存在localStorage。Axios请求拦截器在每个请求Header里加Authorization: Bearer token后端拦截器解析token把userId和role放进ThreadLocal。这里有个细节JWT天然无状态所以不能手动让某个token失效。退出登录时把token加入Redis黑名单。我见过很多学生的项目退出登录只是前端删了localStorage后端token还能用这就留下了安全隐患。角色权限控制我用自定义注解实现。定义RequireRole(ADMIN)注解拦截器里校验当前用户角色是否匹配。比如商品发布接口需要USER商品审核接口需要ADMIN回收员接单接口需要COLLECTOR。这样controller方法上标一个注解就行权限逻辑集中在拦截器代码可读性很高。4.3 商品发布与文件上传商品发布接口接收表单数据包括标题、描述、分类、价格、封面图、详情图。图片上传我建议直接传到服务器本地磁盘再把访问路径存数据库。本地磁盘存储要考虑一个关键问题前端要能直接通过URL访问到图片所以SpringBoot需要配置静态资源映射把自己指定的上传目录映射到/upload/**这个URL路径。application.yml里大致这么配spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB file: upload-path: /data/upload/然后写一个配置类把/data/upload/映射为/upload/**的静态资源。上传后返回的路径就是/upload/2025/03/12/uuid.jpg。常见的坑是文件上传大小限制。默认只有1MB传一张手机拍的大图就直接报错。必须把max-file-size调大。还有文件名别用用户原始文件名会乱码、会重名冲突一定要用UUID重命名。我这里用Hutool的IdUtil.simpleUUID()生成文件名再拼上原始扩展名。4.4 下单流程中的超卖与重复提交处理下单我用的思路是校验商品状态为上架 - 校验用户不是商品本人不能买自己的商品- 生成唯一订单号 - 扣减商品库存如果这个商品限量1件就直接把状态改成“已被下单锁定”- 创建订单。这里我用数据库唯一约束或者乐观锁来处理并发。二手商品单件库存其实可以用一个update语句把商品状态从“上架”改成“已锁定”影响行数为1才下单成功影响行数为0说明商品被别人抢了直接返回“手慢了”。重复提交问题我在前端做了按钮loading限制后端再加一道幂等校验使用Redis的setIfAbsent保存一个orderToken第一次提交成功第二次提交发现key已存在就报“不能重复提交”。4.5 订单超时取消的定时任务实现用Scheduled(cron 0 */5 * * * ?)每5分钟扫一次订单表把“创建时间超过30分钟且状态为待支付”的订单批量改为已取消。注意批量更新时update语句要加上status 0条件防止把已经支付成功的订单也取消掉。这就是典型的条件更新防覆盖问题。如果要把这个方案说得更高级一点可以提RabbitMQ延迟队列方案但毕设阶段定时任务足够满足需求。面试时被问“定时任务有延迟怎么办”可以解释业务上30分钟超时容忍几秒延迟完全没问题如果要求精确到秒级才需要引入延迟队列。5. 前端Vue实现商城、回收预约与后台管理三端页面怎么搭5.1 前端工程初始化与实际目录结构用Vite创建Vue3项目一条命令npm create vitelatest second-hand-mall -- --template vue然后安装依赖。我自己的项目结构是src/ api/ // 按模块拆分的接口文件 assets/ components/ layout/ // 前台布局和后台布局 router/ store/ // Pinia utils/ // axios封装、工具函数 views/ front/ // 商城前台页面 admin/ // 管理后台页面 collector/ // 回收员端页面5.2 Axios请求封装与登录状态管理Axios封装成request.js核心逻辑是请求拦截器从localStorage里取token加到Header上响应拦截器判断code400跳登录页、403提示无权限、500提示服务异常。与后端约定的code规则要前后端一致200成功、400参数错误、401未登录、403无权限、500系统异常。Pinia里建一个useUserStore存用户信息、token、角色。每次页面刷新后重新调用/user/info获取登录用户信息。路由守卫里判断没有token且访问需要登录的页面就跳登录页有token但访问登录页就跳回首页角色不匹配的页面直接提示无权限。这些逻辑在router/index.js的beforeEach里统一拦截。5.3 商城首页与商品列表的实现细节首页我做成轮播图分类导航最新商品热门回收分类四个区块。商品列表页支持分类筛选、关键词搜索、价格排序。列表数据用分页组件配合后端Page对象前端每页12条或8条图片懒加载用Vue的v-lazy指令Element Plus或自定义。商品详情页的核心是展示商品图片轮播、价格、描述、卖家的其他商品、以及“立即购买”和“加入购物车”两个按钮。发布商品页面用el-upload组件传图片到后端action指向后端的/api/file/upload接口。上传后把返回的URL收集进表单数组提交时和后端的数据结构保持一致。5.4 回收预约页面的表单与状态展示回收预约页面是一个分类明确的表单选择回收品类旧衣物、旧书、家电、手机数码、其他、填预约时间el-date-picker选日期和时段、填详细地址、物品描述。提交后跳转到“我的回收预约”列表页按状态显示标签待审核、待接单、待上门、已确认、已完成、已取消。回收员端页面我用了一套独立的侧边栏布局核心是“可接单大厅”和“我的任务”。可接单大厅用卡片列表展示待接单的预约点击接单调用/recycle/accept。我的任务里回收员可以查看自己接下的单点击“确认上门”后填写实际回收金额提交后流程进入用户确认阶段。5.5 管理后台的关键页面管理后台我用经典的上左右布局侧边栏菜单、顶部面包屑、主内容区域。核心页面包括商品审核页待审核商品列表每行有“通过”“驳回”按钮驳回要填原因。商品管理页可以强制下架违规商品列表带状态筛选。回收预约管理页管理员可以查看所有预约单分配回收员或直接驳回。用户管理页启用/禁用账号。数据统计页用v-charts或者ECharts画柱状图/饼图统计每日订单量、回收分类占比等。管理后台核心是表格弹窗表单的组合。Element Plus的el-table、el-dialog、el-form组件都能直接满足重点在于写清楚每行的操作按钮对应哪个接口。6. 联调与部署阶段遭遇的实际问题跨域、时间格式、图片回显和前端代理6.1 跨域问题CORS配置与Vite代理双重方案前后端分离开发时跨域是第一道坎。前端跑在http://localhost:5173后端跑在http://localhost:8080端口不同必然跨域。我做了两层保险后端配置CORS全局允许指定来源前端Vite开发服务器配置server.proxy把/api开头的请求代理到后端。实际开发中我建议以Vite代理为主后端CORS为辅。因为代理模式下前端请求的URL还是同源的不会出现会话和Cookie层面的问题后端CORS则对部署后的生产环境有兜底作用。具体Vite配置是这样server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/user/login开发服务器会把它转发到http://localhost:8080/api/user/login。后端Controller统一加RequestMapping(/api)前缀接口风格清晰。6.2 LocalDateTime序列化问题后端返回Java 8的时间类型如果不做处理前端拿到的是一串数组或者ISO字符串显示很丑。处理方式两种一是在application.yml里全局配置Jackson的日期格式二是在实体类时间字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。我用的全局配置这样不用每个字段都加注解。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这个配置特别容易漏漏了之后前端表格里全是时间戳排查半天还以为是后端代码问题。6.3 MyBatis Plus逻辑删除与字段自动填充逻辑删除是一个很实用、但很多资料不会讲透的点。我在application.yml里配置逻辑删除的全局值实体类字段上加TableLogic注解。这样执行delete操作时MyBatis Plus自动将其转为update语句设置deleted字段为1。所有查询操作也会自动带上deleted0条件。这样实施的好处是数据不真实丢失用户订单和商品关联还能查回来面试时解释清楚这一层能体现你对数据安全的考虑。create_time和update_time两个字段可以用MyBatis Plus的MetaObjectHandler自动填充实体类上标注TableField(fill FieldFill.INSERT)插入时自动写创建时间更新时自动写更新时间。这个方案相比数据库DEFAULT CURRENT_TIMESTAMP好的一点是Java层可控像订单快照这种写入场景可以在逻辑里灵活处理。6.4 图片上传后前端无法访问这个问题我调试过很久。后端图片上传成功路径也存到数据库了但前端访问http://localhost:8080/upload/xxx.jpg返回404。原因就是SpringBoot没有把磁盘的upload目录映射成静态资源URL。解决方式是在配置类里加一个WebMvcConfigurer重写addResourceHandlers方法把/upload/**映射到磁盘路径。部署之后还会遇到第二个坑如果把项目部署到云服务器图片路径不能写死成localhost要换成服务器的公网IP或者域名。所以上传接口返回的URL我建议后端在返回时动态拼接当前请求的域名而不是存一个写死的地址。具体可以用ServletUriComponentsBuilder.fromCurrentContextPath()拿到当前服务的基础地址。6.5 打包部署jar包与前端dist的托管后端部署很简单Maven执行mvn clean package生成target目录下的jar用java -jar xxx.jar运行。前端运行npm run build生成dist目录里面是静态文件。生产环境我建议用Nginx托管前端dist并把/api请求反向代理到后端的8080端口。Nginx配置核心就两大块server { listen 80; server_name your_domain; root /var/www/second-hand-mall/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; } location /upload/ { proxy_pass http://127.0.0.1:8080; } }这样整个系统的外部访问入口只有一个80端口不需要把8080暴露到公网。同时Vue Router要用createWebHistory模式时Nginx还要加一个try_files配置把所有路由都回退到index.html否则刷新页面会404这也是高频坑。7. 答辩和面试环节的高频追问与回答思路7.1 为什么订单表要有订单号为什么不用自增ID当订单号这是一个很典型的追问。订单号不是数据库主键但为什么单独设计一个order_no字段核心原因是业务展示和安全性。自增ID能猜比如你下单ID是100别人抢在你前面下单ID是99平台能通过ID推测出单量而订单号是带有业务规则的编码值比如时间戳随机串不可推测、展示也好看。我用的生成规则是yyyyMMddHHmmss 6位随机数唯一性由MySQL唯一索引兜底。7.2 商品状态机是如何防止非法操作的面试官喜欢问“如果前端绕过按钮直接调用后端接口把已完成的订单取消掉怎么办”这其实是状态机设计要解决的问题。我会回答每个状态流转都在Service层做合法性校验比如取消订单必须先判断当前状态是0待支付确认收货必须先判断当前状态是2待收货。这种硬校验放在后端前端按钮只是UI层面的引导真正的数据安全之后端一层。7.3 回收员抢单怎么避免两个人同时接同一单这是一个典型的并发问题。我的方案是使用update recycle_appointment set collector_id #{collectorId}, status 2 where id #{id} and status 1这样的乐观锁更新数据库行锁保证同时只能有一个回收员更新成功。影响行数为1表示抢单成功为0表示已经被别人抢走。这种方案比先查询再更新要可靠得多也解释了为什么不能让前端“先看再抢”。7.4 从用户浏览到下单整个请求链路是怎么串联的面试官问这类问题是看你有没有完整的调用链意识。我会顺着说用户在商品列表点击商品 - 前端路由跳到详情页 - 详情页onMounted时调用getProductDetail接口 - Axios拦截器自动带上token - 后端Controller接收请求 - Service调用Mapper查询商品信息和卖家信息 - 返回Result对象 - Axios响应拦截器解包 - 前端把数据渲染到页面上。下单流程类似但多了一层创建订单和支付。能够把这个链路讲清楚基本证明你不是只复制了一个项目。7.5 项目里你自认为最复杂、最值得说的部分是什么这一个问题直接决定面试的走向。我会选回收预约的状态流转回收员并发抢单来展开。因为它不只是CRUD还涉及状态机约束、并发控制、事务边界、用户余额变更一致性。可以先讲表结构设计再讲状态机合法流转再讲乐观锁抢单最后讲事务保证余额和状态的同步更新。整个过程有设计有细节比干巴巴说“我做了商品CRUD”强得多。最后再分享一个实打实的经验这个项目从零开始写先把ER图和接口文档定下来再动手敲代码整体开发周期能压缩不少。所有涉及金额的地方一律用BigDecimal别用double所有涉及状态变更的接口响应里都返回当前最新状态前端拿到之后立即刷新页面体验会好很多。我在写这套系统的时候最大的体会是代码只是业务逻辑的载体真正花时间的是把流程想清楚、把状态定义清楚这些前期工作做得越扎实后期写起来越顺利答辩的时候也越有底气。
返回列表