ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Django+Flask的智能物流配送管理系统设计与实践

基于Django+Flask的智能物流配送管理系统设计与实践 做物流调度最头疼的是什么我的答案不是订单多而是车在外边跑调度室里两眼一抹黑。去年接手一个城市配送项目时每天不到三百单用Excel排线靠微信群调度司机到哪了、哪几单顺路、客户催单怎么回应全靠打电话去问。领导要看全局数据我只能手动拉表格凑一张折线图交差数据还滞后半天。后来花了几周时间用Python把整套流程做成了系统订单管理、路径规划、实时定位、可视化大屏一套智能物流配送管理系统。技术栈不复杂Django做Web主框架Flask承担轻量算法接口服务前端可视化用ECharts实时推送走WebSocketRedis负责缓存和状态管理。系统上线后调度员在大屏上就能看到所有车辆的实时位置和运单状态订单自动推荐最优配送路径管理层一眼看清全局指标。这篇文章会把整个系统的设计、选型、核心模块实现、可视化大屏搭建过程以及我踩过的坑完整拆开讲。无论你是准备做课程设计、公司内部工具还是商用系统的初稿这套思路都能直接用。1. 项目整体设计与技术选型为什么是Django Flask双框架1.1 先拆需求智能物流配送管理系统的核心诉求是什么做这类系统最忌讳一上来就敲代码。先把需求拆清楚我归纳成四层。第一层是数据层。订单、运单、客户、配送点、车辆、司机这些基础数据的管理。物流场景的数据结构比普通系统复杂比如一张订单可能拆成多个包裹多个订单可能合并成同一个运单配送地址要带经纬度还有时效窗口。这些关系建不好后面全是坑。第二层是业务层。调度、路径规划、运单状态流转、异常处理。这一层承载智能两个字。系统要根据当日的订单分布给调度员推荐一组合理配送路径司机点击签收后运单状态自动流转不需要人工改表格。第三层是可视化层。把业务数据实时呈现出来。调度员看地图和运单状态管理层看核心指标客服看订单进度。这一层直接决定了系统好不好用。第四层是集成层。需要对接外部系统比如车载GPS设备上传位置、司机App上报状态、短信通知客户。我先把这四层列清楚然后才去选技术方案。1.2 Django还是Flask我的选择与理由这个项目最终用了Django Flask双框架而且不是炫技是实际需求驱动的。Django是典型全家桶自带ORM、Admin后台、用户认证、CSRF防护。物流管理系统需要大量后台管理功能比如运营人员维护运价、客户黑名单、配送范围。Django的Model和Admin可以极大提升开发效率尤其是需求频繁调整的初期阶段。举个例子我只需要定义一个Model注册到Admin运营人员就能在线增删改查省下大量手写管理页面的时间。Flask是轻量级中的轻量级适合做简单接口服务和中间件。在这里我把路径规划算法单独拆成一个Flask微服务前端请求进来后这个微服务负责调用算法模块、做参数校验、格式化返回结果。为什么要把算法拆出去路径规划是CPU密集运算独立成服务后可以单独扩容不会影响主系统的Web接口响应。如果以后算法升级或者换用更复杂的求解器也不用动主工程代码。Django和Flask的分工用一张表说清楚维度DjangoFlask定位Web主框架业务系统轻量API服务算法调用承担工作订单模块、权限、Admin后台、ORM路径规划接口、地图服务代理、数据采集部署方式Daphne NginxGunicorn独立进程适用场景复杂业务建模、后台管理快速提供一个独立HTTP接口实际开发中双框架的沟通成本几乎为零。Django通过HTTP请求调用Flask的接口Flask返回JSON序列化的结果。两个服务可以部署在同一台服务器上也可以分开部署。1.3 可视化方案选型为什么是ECharts可视化第一原则是别重复造轮子。物流配送管理系统需要展示的数据形式折线图订单量趋势、柱状图区域订单分布、地图车辆位置与轨迹、仪表盘准点率、完成率。这些ECharts全部覆盖纯JavaScript实现配置简单文档完善。有人问过为什么不用DataV或者自研图表DataV更适合专业大屏场景但定制成本高、依赖平台自研图表灵活但工作量大、没有必要。ECharts是中等复杂度项目最稳妥的选择。地图部分处理方式略特殊。底图用ECharts的Geo/MAP系列加载GeoJSON区域着色就靠ADCode关联数据。车辆实时轨迹和动效则是ECharts的lines effectScatter系列。具体代码后面详细讲。2. 核心功能模块拆解与建模2.1 订单与运单的数据模型设计物流系统数据结构我建议围绕订单 运单 配送节点来设计这三个概念想清楚整个系统就稳了一半。订单Order是业务侧概念客户下单包含商品信息、收货地址、时效要求。运单Waybill是履约侧概念一个订单可能被拆分成多个运单比如一个订单有10件货仓库分了两个包裹一个运单也可能包含多个订单同一客户同一地址多笔订单合并配送。配送节点DeliveryNode是路径规划的最小单位一个运单通常有多个节点比如出库点、客户A、客户B每个节点对应一次停靠包含经纬度、计划到达时间、实际到达时间、状态。这样建模的好处是订单和履约解耦。仓库分拣拆包、司机合单配送都不需要动订单数据。表结构的关键字段# 订单表 class Order(models.Model): order_id models.CharField(max_length32, uniqueTrue) sku_list models.JSONField() # 商品明细简化起见用JSON address models.CharField(max_length200) lng models.FloatField() lat models.FloatField() time_window_start models.DateTimeField(nullTrue, blankTrue) time_window_end models.DateTimeField(nullTrue, blankTrue) status models.CharField(max_length20, choicesORDER_STATUS, defaultPENDING) # 运单表 class Waybill(models.Model): waybill_id models.CharField(max_length32, uniqueTrue) orders models.ManyToManyField(Order) vehicle models.ForeignKey(Vehicle, on_deletemodels.SET_NULL, nullTrue) driver models.ForeignKey(Driver, on_deletemodels.SET_NULL, nullTrue) status models.CharField(max_length20, choicesWAYBILL_STATUS, defaultASSIGNED) total_weight models.FloatField(default0) total_volume models.FloatField(default0) # 配送节点表 class DeliveryNode(models.Model): waybill models.ForeignKey(Waybill, related_namenodes, on_deletemodels.CASCADE) seq_no models.IntegerField() # 序号决定访问顺序 lng models.FloatField() lat models.FloatField() planned_arrive_time models.DateTimeField(nullTrue, blankTrue) actual_arrive_time models.DateTimeField(nullTrue, blankTrue) status models.CharField(max_length20, defaultPENDING)状态机设计这里要重点提一下。运单状态我只允许五态流转待分配PENDING、已调度ASSIGNED、配送中DELIVERING、已送达COMPLETED、异常EXCEPTION。每一次状态变更是通过代码显式调用的不允许任意跳转。这个设计一开始显得多此一举上线后才发现香。没有约束的状态字段过两个月就会被各种临时改动搞成一团乱麻。2.2 路径规划从最近邻算法到2-opt优化路径规划是智能的核心。一开始不需要上遗传算法、蚁群算法这些重型武器。一个基础可用的方案是最近邻算法 2-opt局部搜索。最近邻思路非常直观从起点出发每次选择离当前点最近的未访问客户点作为下一个配送点。算法简单、执行快得到的路径虽然不是最优解但通常不会离谱得离谱。两点距离不能直接套平面欧氏距离公式因为经纬度是球面坐标。用Haversine公式计算import math def distance(p1, p2): # 输入: {lat: float, lng: float} R 6371.0 # 地球平均半径, 单位km phi1 math.radians(p1[lat]) phi2 math.radians(p2[lat]) d_phi math.radians(p2[lat] - p1[lat]) d_lambda math.radians(p2[lng] - p1[lng]) a math.sin(d_phi / 2) ** 2 math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2 return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) def nearest_neighbor(points, start_index): points: 列表, 每个元素是{lat: , lng: , id: } unvisited set(range(len(points))) route [start_index] unvisited.remove(start_index) current start_index while unvisited: nearest min(unvisited, keylambda p: distance(points[current], points[p])) route.append(nearest) unvisited.remove(nearest) current nearest return route这段代码在真实地理坐标系下可用。但最近邻算法有个明显短板每一步只贪眼前最近路径后期可能出现交叉或绕路。2-opt是经典局部优化手法在路径中选两条边尝试交换节点顺序如果总距离变短就采纳。迭代到没有改进为止。def two_opt(route, points, max_iters100): best_route route[:] best_dist compute_total_distance(best_route, points) improved True while improved and max_iters 0: improved False for i in range(1, len(best_route) - 2): for j in range(i 1, len(best_route) - 1): new_route best_route[:i] best_route[i:j1][::-1] best_route[j1:] new_dist compute_total_distance(new_route, points) if new_dist best_dist - 1e-6: best_route new_route best_dist new_dist improved True max_iters - 1 return best_route实测下来20个配送点以内2-opt毫秒级完成路径长度比纯最近邻缩短10%~20%。如果单辆车配送点超过50个建议升级到OR-Tools或遗传算法。但我还是建议先把经典算法吃透再上重型工具。OR-Tools求解器功能确实强大但约束建模有学习成本。系统初期先把基础跑通后续有需要再替换更优解。2.3 实时数据流从GPS上报到大屏刷新物流系统的实时性体现在三个关键位置。第一司机端App或者车载GPS设备每隔几秒上报经纬度、速度、运单状态。第二后端上报接口接收数据更新运单当前节点位置。第三可视化大屏通过WebSocket接收新数据实时刷新地图和指标。上报接口格式很简单就是POST JSON{ waybill_id: WB20240831001, vehicle_id: V202, lng: 120.1532, lat: 30.2851, speed: 32.5, timestamp: 2024-08-31 14:23:45, status: DELIVERING }这里有一个很重要的设计和经验GPS上报如果每次都写MySQL并发一高必然扛不住。假设100辆车每5秒上报一次每秒就有20个写入请求数据库压力大而且大屏读取实时位置时还要去数据库查更慢。我的做法是分两层实时数据先进Redis用Hash结构保存每个车辆的最新位置key是vehicle_gps:{vehicle_id}字段是lng、lat、speed、update_time。WebSocket服务从Redis读取最新位置并推送大屏不直接查MySQL。历史轨迹数据通过另一个定时任务每隔5分钟从Redis批量落库用于后续分析回放。这样实时路径走Redis历史路径走MySQL各司其职。WebSocket推送的延迟能压到几百毫秒以内。3. 可视化大屏的实现细节3.1 大屏布局与核心指标设计可视化大屏并不是把图堆满整个屏幕就行本质是通过布局引导视觉焦点。我的设计思路是这样的中间是地图这是整个大屏的视觉焦点展示所有车辆的位置和运行轨迹。左侧上下分别是订单量趋势折线图、区域订单分布柱状图。右侧上下分别是在途车辆状态列表、关键指标仪表盘。核心指标只保留6个今日订单量、在途车辆数、配送完成率、平均配送时长、准点率、异常单数量。选这些是因为它们能直接反映配送核心健康度。有些团队会把十几个指标全堆上去信息过载领导反而不知道看哪里。我从第一版十来个图表砍到最后一屏6个图表地图信息密度和信息清晰度才真正平衡。配色方面大屏背景用深色底#10182f图表用蓝色系加橙色系作主色。深色底在监控室光线环境下视觉干扰小数据点亮起来的效果更突出。我封装了一个统一的大屏样式文件每个图表共用了网格、坐标轴颜色、提示框背景避免风格不统一显得很乱。3.2 ECharts在Django模板中的正确集成方式Django模板集成ECharts最大的坑是数据注入方式。别人可能会告诉你把数据塞进模板变量再渲染实际开发中我最推荐的方式是模板只负责搭外壳图表数据全部通过Ajax/fetch请求API接口获取JSON。原因是模板变量里的JSON字符串会被Django自动HTML转义引号、尖括号都会变成实体JavaScript直接解析就会报错。而且把大量数据直接渲染到HTML源码里页面体积大、加载慢数据更新时整个页面都得刷新体验很糟糕。正确做法示例。Django视图返回JSONfrom django.http import JsonResponse from django.db.models import Count from .models import Order def order_trend_api(request): 按日期统计订单量用于可视化大屏的折线图 data ( Order.objects .extra(select{day: date(create_time)}) .values(day) .annotate(totalCount(id)) .order_by(day) ) result [{day: item[day], total: item[total]} for item in data] return JsonResponse(result, safeFalse)模板中的JavaScript单独通过fetch获取并渲染script src{% static echarts/echarts.min.js %}/script script fetch(/api/order/trend/) .then(res res.json()) .then(data { var chart echarts.init(document.getElementById(trendChart)); chart.setOption({ xAxis: { type: category, data: data.map(item item.day) }, yAxis: { type: value }, series: [{ type: line, data: data.map(item item.total), smooth: true }] }); }); /script这里有两个细节。第一ECharts实例在窗口变化时要调用chart.resize()不然图表宽度会塌陷。第二如果图表所在容器初始是隐藏的比如切换Tab才显示也会出现渲染异常需要手动调用resize。这两点几乎每次做可视化都会遇到顺手写了全局监听。window.addEventListener(resize, function() { // 遍历所有已初始化的图表实例 for (var key in echarts.getInstanceByDom) { // 实际实现需要维护一个chart实例数组 chartInstances.forEach(function(c) { c.resize(); }); } });3.3 WebSocket实时推送Django Channels的实现要点大屏实时刷新如果靠前端定时轮询浪费资源而且延迟高。WebSocket是全双工通道服务端能主动把数据推给浏览器大屏刷新从假轮询变成了真推送。Django里实现WebSocket主流方案是Django Channels。核心步骤如下。第一步安装并配置pip install channels channels-redis# settings.py ASGI_APPLICATION myproject.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: {hosts: [(127.0.0.1, 6379)]}, } }第二步编写consumer# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class DispatchConsumer(AsyncWebsocketConsumer): async def connect(self): await self.channel_layer.group_add(dispatch, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(dispatch, self.channel_name) async def send_position(self, event): await self.send(text_datajson.dumps({ type: position, data: event[data] }))第三步在GPS上报接口中推送数据from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( dispatch, {type: send.position, data: position_data} )这里最容易踩的坑就是group_send是异步方法在同步视图中必须用async_to_sync包一层。很多新手忘了这一步直接调用然后报错。生产环境中运行Django WebSocket要用Daphne启动ASGI服务而不是Gunicorn。本地开发runserver就能跑但如果用了uvicorn myproject.asgi:application也可以。3.4 地图轨迹可视化车辆位置和移动动效地图部分是大屏最容易出效果、也最容易翻车的地方。我的方案是ECharts geo组件的GeoJSON底图叠加effectScatter车辆点。车辆位置数据直接来自Redis缓存的实时坐标。车辆点展示series: [{ type: effectScatter, coordinateSystem: geo, data: vehiclePositions.map(item ({ name: item.driver_name, value: [item.lng, item.lat], vehicle_id: item.vehicle_id })), symbolSize: 12, rippleEffect: { brushType: stroke } }]轨迹线用lines系列series: [{ type: lines, coordinateSystem: geo, data: [{ coords: [[startLng, startLat], [midLng, midLat], [endLng, endLat]] }], lineStyle: { color: #1890ff, width: 2, opacity: 0.6 } }]这里必须提醒一个关键问题坐标系一致性。国内主流地图底图用的是GCJ02火星坐标系而GPS设备返回的是WGS84坐标。如果直接把GPS原始坐标画在地图上车辆位置会偏移几百米。这个坑几乎是所有地图可视化项目都要遇到的我一开始也踩了车辆标注全部跑偏后来加了一遍坐标转换才解决。在高并发或大数据量场景车辆几百辆、轨迹点上万不能直接在ECharts上画全部点。地图缩放级别低时只显示区域聚合汇总zoom级别高时才显示具体车辆点。ECharts scatter系列支持large: true走WebGL渲染几万点也能扛住但要配合范围内动态取数。4. 数据库设计与性能优化4.1 Redis在物流系统里到底承担什么角色Redis在这套系统里不只是缓存它的三个关键角色都要用好。第一个是实时位置缓存。GPS上报的车辆位置写入Redis Hash接口读取车辆位置时走Redis而不是MySQL毫秒级返回。假设100辆车每秒上报4亿次如果直接写数据库数据库连5分钟都扛不住。第二个是在线状态管理。用Redis的Set或Sorted Set记录当前在线车辆key设置过期时间为10秒司机断线后10秒内自动被清除。大屏上对应车辆点会置灰或消失运营人员就能及时发现异常离线车辆。第三个是消息广播。Django Channels的Channel Layer底层就是Redis Pub/Sub。用Redis做广播通道可以支持多个WebSocket服务节点。将来部署多台服务器在A节点推送消息B节点也能收到实现跨节点广播。开发调试时Redis可视化客户端推荐RedisInsight官方出品或Another Redis Desktop Manager。命令行排查也行但看列表和Hash结构用可视化工具更直观特别是检查实时车辆位置时一眼就能看清哪个车辆key存活、数据格式对不对。4.2 Django ORM查询优化从慢查询到秒开可视化大屏的接口如果每次都全表扫描数据量增长后会非常卡。优化手段主要三个。第一是索引。高频查询字段一定要加索引Order.create_time、Waybill.status、DeliveryNode.waybill_id、DeliveryNode.status。联合查询多的场景比如按状态和创建时间过滤运单考虑建联合索引。第二是避免N1查询。Django的ORM默认懒加载获取运单列表时如果循环取waybill.driver.name每行都会多一次SQL查询N条运单就是N1次查询。用select_related(driver, vehicle)一次性JOIN出关联对象waybills Waybill.objects.select_related(driver, vehicle).filter(statusDELIVERING)第三是聚合下推到数据库。不要先取几千条记录再到Python里慢慢遍历统计用annotate和aggregate让数据库完成聚合from django.db.models import Count node_status_summary ( DeliveryNode.objects .filter(actual_arrive_time__datedate.today()) .values(status) .annotate(totalCount(id)) )这种写法生成的SQL是GROUP BY status数据库完成聚合返回只有几行比逐条统计快一个数量级。大屏实时指标还需要预计算。比如今日订单量这个指标写一个Celery定时任务每5分钟聚合一次结果存Redis前端接口直接读Redis响应时间控制在10ms以内。大屏同时多个人访问也不会把数据库打满。4.3 大数据量场景下的大屏性能数据量涨到几万张订单、几百辆在途车辆时大屏依然要流畅需要一些额外手段。地图点聚合是第一步。当车辆数量超过300辆而且集中在一个狭小区域时ECharts默认scatter就会出现大量重叠点。开启large: true模式让WebGL渲染同时调整symbolSize随聚合数量缩放视觉上就能接受。轨迹数据降采样是第二步。历史轨迹点如果几万个全量展示ECharts渲染会有明显卡顿。我的处理办法是按5秒或10秒采样24小时轨迹从几万点降到几千点展示效果几乎一致渲染压力骤减。第三步是数据分页懒加载。订单列表、轨迹详情这些模块不要一次全部加载用前端滚动加载或接口分页。地图缩放时只加载当前视野范围内的配送点视野外的数据不渲染。5. 项目部署与上线经验5.1 Django WebSocket的生产级部署开发环境跑runserver没问题生产环境不能这样。我的部署架构是Nginx做反向代理和静态文件服务Daphne运行ASGI服务支撑Django ChannelsRedis和MySQL独立部署。Nginx关键配置upstream daphne_backend { server 127.0.0.1:8000; } server { listen 80; server_name your_domain.com; location /static/ { alias /var/www/your_project/static/; } location /ws/ { proxy_pass http://daphne_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; } location / { proxy_pass http://daphne_backend; proxy_set_header Host $host; } }WebSocket代理配置有个非常容易踩的坑Upgrade和Connection这两个请求头必须带上。如果不带浏览器会一直卡在握手阶段控制台报WebSocket connection failed。还有一点Nginx的proxy_read_timeout默认只有60秒WebSocket长连接时间长了会被断开建议设置长一点。启动命令方面我用Daphnedaphne -b 0.0.0.0 -p 8000 myproject.asgi:application配合supervisor或systemd做进程守护确保进程崩溃后能自动拉起。5.2 静态文件与模板标签的经典问题vscode写img标签在Django的static文件中显示不了这个问题几乎每个Django新手都会遇到九成是静态文件配置没走对。正确姿势分三步。第一步settings.py配置STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] STATIC_ROOT BASE_DIR / staticfiles第二步模板顶部必须要{% load static %}然后引用img src{% static images/logo.png %} altlogo不要直接写成/static/images/logo.png硬编码虽然开发模式下能工作但配合部署时的collectstatic路径会出问题。第三步上线前运行python manage.py collectstatic把各app的静态文件统一收集到STATIC_ROOT目录由Nginx直接服务。注意一旦设置DEBUGFalseDjango自己就不会再处理静态文件了全权交给Nginx。很多人在服务器上看不到图就是漏了collectstatic这一步。5.3 日志、定时任务与异常监控物流系统7x24小时运行没有监控等于裸奔。我加了三个基础手段。第一是Django日志配置INFO和ERROR分开存ERROR日志用RotatingFileHandler切割避免磁盘被日志撑爆。配置示例如下LOGGING { version: 1, handlers: { error_file: { level: ERROR, class: logging.handlers.RotatingFileHandler, filename: /var/log/project/error.log, maxBytes: 50 * 1024 * 1024, backupCount: 5 }, }, root: {handlers: [error_file], level: ERROR} }第二是Celery定时任务负责每日报表生成、轨迹数据清理、大屏指标预计算。Celery Beat加Redis做任务调度非常稳定我用的任务周期有每5分钟、每小时、每天三种。第三是异常告警。写了个check脚本定期检查关键接口的响应时间超过阈值就往钉钉群里发消息。刚起步没必要上太重型的监控平台一条最简单的告警脚本就能发现绝大多数服务异常。6. 常见问题与避坑实战记录6.1 问题速查表整个项目从0到1我遇到的高频问题整理成一张速查表方便遇到同样问题时自查现象原因解决方案大屏图表不渲染fetch返回的JSON结构不对或图表容器宽度高度为0先在console.log输出data确认结构给图表容器设置显式宽高WebSocket一直connection failedNginx没配Upgrade头 / Channels没启动加上Upgrade和Connection: upgrade用Daphne启动ASGI图片加载不出来静态文件路径配置错误 / collectstatic未执行检查STATICFILES_DIRS和模板static标签上线后执行collectstatic地图车辆位置偏移WGS84与GCJ02坐标系混用统一转换为GCJ02坐标大屏数据刷新慢每次都直接查MySQL热点指标预计算存Redis前端接口读RedisDjango删除对象报外键错误关联数据未处理外键约束阻止删除先删关联节点再删运单或设置on_deletemodels.CASCADE车辆实时位置丢失Redis key过期时间太短每次上报都刷新过期时间设置为10秒以上ECharts图表窗口缩小后变形未监听resize事件window.addEventListener(resize, chart.resize)6.2 我的三个深刻心得第一数据模型是整个系统的地基。我最早把订单和运单设计成一张表导致后面扩展合单配送拆包发运时痛苦不堪花了将近两天重构数据模型。如果你现在刚起步请一定把订单、运单、配送节点这三个概念分开建模前期多花几个小时后期省下几天时间。第二前端可视化不是堆图表。大屏的核心是帮用户快速做判断不是展示技术能力。我第一版把十来个图表全堆上去领导反而不知道该看哪个。砍掉冗余指标后只保留6个关键指标加地图信息清晰恢复快速的视觉焦点。第三性能优化要基于真实数据量不要一开始就玩骚操作。系统初期几百单随便怎么写都很快等到数据涨到几万单才发现慢查询一堆。我的建议是上线前用SQL explain检查核心查询上线后监控慢SQL定位到实际问题再去优化否则很容易做无用功。最后分享一个小技巧如果你也在Django里做可视化大屏可以给图表数据接口加上cache_page(60)装饰器把接口缓存一分钟。前端图表即使被频繁轮询后端也不会被瞬时并发打穿。我实测下来这招让大屏接口的稳定性提升非常明显。
返回列表