news 2026/9/9 1:13:29

基于DQN的交通信号灯相位时间优化:从SUMO仿真到实战调参

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DQN的交通信号灯相位时间优化:从SUMO仿真到实战调参

简介:面向毕业设计与课程设计场景,资源提供了一套基于开源SUMO交通仿真平台和深度强化学习DQN算法的信号灯相位时间优化项目。整套代码采用Python编写,覆盖路网构建、仿真交互、模型训练与结果分析等关键环节,适合对智能交通和强化学习感兴趣的开发者快速上手。项目共32个文件,包含路网配置、地图数据、算法脚本、实验结果表格及说明文档,其中XML和OSM文件用于构建仿真环境,PY文件为核心控制逻辑,压缩包仅533KB,部署轻量。目前已有545人学习使用。通过阅读源码,可以理解状态特征选取、动作输出方式和奖励函数设计,并掌握将DQN应用于交通信号控制的完整流程,理解从路网搭建、仿真交互到模型训练与收敛评估的整个链路,便于在此基础上扩展优先级决策或迁移至其他路网场景。 大概半年前我准备一个交通仿真的课程项目时,盯着SUMO仿真界面里的红绿灯发呆:东西方向已经堵成一片,南北方向却一辆车都没有,可信号灯依然按着固定配时在傻傻地放行。那一刻我意识到,交通信号灯控制远比看起来复杂,它本质上是一个动态环境下的序列决策问题,而这类问题恰好是强化学习的看家本领。于是我基于Python + SUMO仿真平台,用DQN做了一套能根据实时车流调整交通信号灯相位时间的方案,整套源码也整理成了一个开源项目。这篇文章就把这套方案从环境搭建、算法设计到训练调参的完整过程拆给你看,适合正在做毕设、参加竞赛,或者想入门强化学习在交通领域应用的同学参考。

1. 信号灯控制的痛点:为什么固定配时总是差一口气

1.1 交通流本身就是"非平稳"的

传统路口信号机用的都是固定配时方案,也就是预先算好每个相位的红绿灯时长,然后按周期循环执行。这种方式在车流比较稳定的时候勉强够用,但一旦遇到早晚高峰、学校放假、突发事故、旁边路口修路,车流特征就会完全变化。固定配时没有任何感知能力,只能按部就班地放行,结果就是绿灯方向没车,红灯方向排队几百米,整个路口通行效率大幅下降。

很多同学可能会觉得"那我用多时段配时方案不就行了,高峰一套、平峰一套"。这确实比固定配时强,但本质上还是开环控制,交通流一旦出现计划之外的波动,多时段方案照样失灵。交通仿真里我们经常说一句话:交通流本身是非平稳的,每分钟的车流量都在变,所以真正有效的策略必须是闭环的、能根据实时状态做出反应的。

1.2 DQN在信号灯场景里到底做什么

用DQN解决信号灯问题,其实是把"信号灯控制器"当成一个智能体,让它通过与环境不断试错来学习一套控制策略。每个决策时刻,智能体观察当前路口的状态,比如各个方向的车队有多长、车辆等了多久,然后决定当前绿灯相位应该继续延长,还是切换到下一个相位。切换之后,环境发生改变,智能体会得到一个奖励信号,比如等待车辆减少了多少。经过成千上万次试探,DQN会逐渐学会什么情况下该延长、什么情况下该切换。

用一个生活化类比:固定配时相当于一台自动售货机,投币后永远出同一瓶饮料;DQN相当于一个有经验的老交警,他会看一眼各个方向的车流,再决定先放谁、放多久,而且随着经验积累,判断越来越准。

1.3 这个项目适合谁、能学到什么

如果你正在做智慧交通方向的毕设或竞赛项目,这个题目几乎是"标配";如果你想入门强化学习但不想跑那种玩具环境,交通信号灯也是一个很合适的实战场景。跑通这个项目你至少能收获四样东西:

  • SUMO这个专业交通仿真器的基本使用,包括路网生成、车流配置、TraCI接口调用;
  • 强化学习三要素——状态、动作、奖励函数——如何映射到真实工程问题里;
  • DQN的完整训练链路,包括经验回放、目标网络、epsilon-greedy探索;
  • 一套可以扩展的多路口信号灯控制源码框架,后续换算法(比如DQN换PPO)只需要改agent部分。

2. SUMO环境搭建与TraCI通信:先说环境再谈算法

2.1 SUMO安装与环境变量配置

SUMO全称Simulation of Urban MObility,是一款开源的微观交通仿真平台。所谓微观,就是每辆车都有独立的加速度、最大速度、换道行为,能够比较真实地模拟交通流。

安装方式取决于操作系统。Windows用户可以直接去官网下载安装包,也可以用包管理器安装;Ubuntu等Linux系统推荐命令安装,macOS用brew也能装。安装完成后需要确认SUMO_HOME环境变量指向安装目录,并且把sumo和netconvert这些可执行文件所在目录加入PATH,否则后续Python调用会找不到命令。

验证安装可以打开终端执行一句命令:

sumo --version

如果能看到版本号,说明安装成功。要注意的是,SUMO更新迭代很快,不同大版本的TraCI接口函数会有差异,我这边用的是1.15.0版本,如果你用的版本比较新,个别接口名可能需要微调。

2.2 搭建一个十字路口仿真场景

训练信号灯控制,先得有一个能反复实验的路口场景。最经典的就是单十字路口,两个方向交叉,每条进口道双向各两车道,中间路口设置信号灯。SUMO里搭建场景有两种方式:一是用自带的netedit图形化编辑器画,二是用XML文件定义节点和边,然后通过netconvert命令生成路网。图形化适合微调,命令行适合自动化生成,我项目中用的是后者的思路。

路网的基本结构是nodes.xml里定义节点坐标,edges.xml里定义连接的边。一个简单十字路口可以这样描述:

<nodes> <node id="N" x="0" y="300"/> <node id="S" x="0" y="0"/> <node id="E" x="300" y="150"/> <node id="W" x="0" y="150"/> </nodes>

定义好节点后,用netconvert生成路网时给中间节点加一个type="traffic_light"属性,系统就会自动为该路口分配一套两相位信号灯方案。车流文件rou.xml则定义了车辆什么时候出现、从哪个进口道到哪个出口道,可以通过flow标签灵活设置每小时车流量。

最后用sumo.sumocfg把这些文件汇总起来。训练时只需要在Python里用一行命令启动仿真:

import traci traci.start(["sumo", "-c", "config.sumocfg"])

训练阶段我强烈建议不用sumo-gui,因为图形界面会拖慢仿真速度。要看可视化效果时再换回sumo-gui即可。

2.3 TraCI接口:让Python成为"交通指挥官"

TraCI(Traffic Control Interface)是SUMO提供的通信接口,基于TCP协议。Python通过import traci连接上仿真进程后,就能实时读取路网状态、控制车辆和信号灯,相当于给仿真环境开了一个后门。

日常用得最多的TraCI方法大概是这几个:

功能方法说明
获取当前仿真时间traci.simulation.getTime()单位秒
获取车道排队车辆数traci.lane.getLastStepVehicleNumber(laneID)通过具体车道ID查询
获取路段等待时间traci.edge.getWaitingTime(edgeID)返回所有车辆等待总秒数
获取当前信号灯相位traci.trafficlight.getPhase(tlsID)返回相位序号
设置当前相位剩余时间traci.trafficlight.setPhaseDuration(tlsID, dur)控制绿灯延长/缩短
切换相位序号traci.trafficlight.setPhase(tlsID, idx)直接跳转到指定相位

项目里为了统一获取"当前路口的综合状态",我会封装一个StateExtractor类,把所有TraCI查询集中在一起,这样训练主程序看起来更干净,后面加特征也好维护。

3. DQN建模三件套:状态、动作、奖励函数的设计逻辑

3.1 状态空间:给智能体一双能看路况的眼睛

状态空间的设计直接决定智能体能不能学会策略。如果只给一个"当前时间",DQN什么都学不会;如果给全路网几百辆车的坐标,输入维度太大,训练难度又会爆炸。需要找到一组既能描述路况、又足够精简的特征。

我最终使用的状态向量由一个路口的关键信息拼装而成:

  • 四个进口道方向的排队车辆数(单位:辆);
  • 四个方向的平均等待时间(单位:秒);
  • 当前相位编号和当前相位已经持续的秒数;
  • 路口总等待时间(单位:秒)。

之所以要把"当前相位编号"和"相位已持续时长"放进去,是因为DQN的动作决策跟当前信号灯在哪个状态密切相关。同时,排队长度和等待时间这两类特征一个体现空间拥堵,一个体现时间延误,组合起来能帮助智能体平衡"放行效率"和"公平性"。

有一点必须提醒:喂给网络的输入一定要做归一化。排队长度可能到几十辆,等待时间可能到几百秒,这些数值直接放在一起,会让神经网络初期的梯度被大数值维度主导,导致训练很不稳定。我的做法比较简单,排队车辆数除以路口最大车道容量,等待时间除以一个经验上限(比如180秒),把大部分特征压到0到1范围内。

3.2 动作空间:把"相位时间调整"变成DQN的输出

标题里的"相位时间调整"在代码上其实可以抽象成两类动作设计方式。

第一种是把绿灯时间离散成几个固定档位,比如动作0表示延长5秒,动作1表示延长10秒,动作2表示延长15秒,动作3表示立即切换相位。这种方式动作空间更细,但会让训练收敛变慢,因为几个延长时间选项产生的状态差异很小,Q值难分高下。

第二种是把动作简化为二分类:继续延长当前绿灯相位,或者结束当前相位、进入下一相位。我最终选择的是这个方案。原因很简单:单路口信号灯控制的本质决策就是"切换还是不切换",至于延长多久,可以通过决策频率来间接控制。我的决策间隔设为10秒,也就是说每10秒智能体评估一次,如果选择"保持",绿灯自动加10秒,如果选择"切换",信号灯就会在下一个仿真步进入黄灯过渡,再进入下一相位。

这个设计还有一个好处:它完全符合真实信号机的工作逻辑。现实中信号灯并不可能每秒钟都在调整,决策频率过低或过高都不合理。决策间隔太短会导致频繁切换,形成"绿灯刚亮就灭"的抖动现象;间隔太长又会让智能体反应迟钝。10秒是我在试验中觉得平衡性最好的值。

3.3 奖励函数:排队长度变化量怎么量化

奖励函数是强化学习最容易被忽略、但也最决定成败的部分。信号灯控制最常见的优化目标是减小车辆平均等待时间、减少排队、提高通行量,但这些目标直接作为奖励并不好优化,因为它们都是长期累积量,反馈稀疏且延迟严重。

我采用的奖励公式是:

R = - (L_t - L_{t-1})

其中L_t是当前时刻路口所有进口道的排队车辆总数,L_{t-1}是上一决策时刻的排队车辆总数。这个公式的含义非常直观:如果这次决策让排队车辆减少了,奖励为正;让排队增加了,奖励为负。智能体最大化累积奖励,本质上就是在最小化整个仿真时段内的排队增量。

为什么不用"平均等待时间"作为直接奖励?因为等待时间的变化到决策之间有时序滞后,而且受偶发车流波动影响大,方差很高。排队长度变化量则是一个相对平滑、立竿见影的指标,SR(Stability)也更好。如果你想进一步优化,可以在奖励里加一个"切换惩罚项",比如每次切换相位时额外减一个固定值,防止智能体频繁抖动。我试验后发现,加了切换惩罚反而会导致智能体过于保守,该切换时不切换,所以最终版本里没加。

3.4 三件套的整体联动

状态、动作、奖励这三者不是孤立的,它们共同定义了马尔可夫决策过程。具体到运行流程,每个决策时刻智能体根据状态选择动作,动作改变信号灯相位时间,相位时间影响车流运行,车流运行产生新的状态和奖励,然后进入下一个决策循环。只有三者都合理,DQN才能真正学到东西。我见过不少同学跑不出效果,第一反应是改网络结构和学习率,其实问题往往出在状态特征不够或奖励函数设计不当上。

4. 训练流程与源码拆解:从经验回放到目标网络

4.1 网络结构与超参数设置

DQN的核心是用深度神经网络来逼近Q函数,也就是"在当前状态下,每个动作能带来的未来累计奖励期望"。我的网络结构非常简单,三层全连接:

import torch.nn as nn import torch.nn.functional as F class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.fc1 = nn.Linear(state_dim, 128) self.fc2 = nn.Linear(128, 128) self.fc3 = nn.Linear(128, action_dim) def forward(self, x): x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) return self.fc3(x)

信号灯控制的状态维度本就不高,一两百维以内,没必要上CNN、Transformer这类复杂的结构。全连接网络加两到三层隐藏层就完全够用,隐藏层宽度设为128或256即可,参数太多反而容易过拟合。

经验里比较关键的超参数我整理成了表格,方便直接抄作业:

参数推荐值说明
学习率1e-4调大容易出现Q值发散
折扣因子gamma0.95兼顾短期排队和长期效果
经验池容量20000太大旧样本占比过高
batch size64常用值,32偏慢128偏抖
目标网络同步步数500太频繁等于没有目标网络
epsilon初始值1.0早期充分探索
epsilon最小值0.05保留一定随机性
epsilon衰减步数5000线性衰减

4.2 训练主循环的流程拆解

训练主循环的代码骨架并不复杂,关键是理解每个步骤为什么存在。最核心的逻辑如下:

for episode in range(EPISODES): traci.start(sumoCmd) state = get_state() total_reward = 0 while traci.simulation.getMinExpectedNumber() > 0: action = epsilon_greedy(state, epsilon) reward, next_state = execute_action(action) replay_buffer.push((state, action, reward, next_state, False)) if len(replay_buffer) > BATCH_SIZE: train_step() state = next_state total_reward += reward traci.close()

每个episode相当于一次完整的仿真,比如模拟3600秒的早高峰交通。仿真开始后,循环不断读取状态、选动作、执行动作、攒经验、更新网络,直到所有车辆都离开路网,一个episode结束。

execute_action这个函数是关键。当智能体选择"保持当前相位"时,我会调用traci.trafficlight.setPhaseDuration(tlsID, currentRemain + 10),让当前绿灯相位继续延长10秒;当选择"切换"时,就把当前相位的剩余时间设为1秒,让SUMO自然进入黄灯过渡相位,再切换到下一相位。这样做的目的是把相位切换的过渡处理交给SUMO内部机制,避免手动跳相位导致车辆冲突。

4.3 经验回放和目标网络到底解决了什么

DQN相比传统Q-learning最大的改进就是引入了经验回放和目标网络,这两个机制都是在解决训练稳定性的问题。

经验回放是把智能体探索过的所有(state, action, reward, next_state)存进一个缓冲区,训练时按batch随机采样。为什么要随机采样?因为强化学习的样本之间存在高度时间相关性——车辆排队、放行这个过程中,相邻几步的状态几乎差不多,如果直接用连续样本训练,网络会在这段局部模式上来回震荡,学不到全局规律。随机采样打破了这个相关性,让每次梯度更新尽量来自于多样的、独立分布的经验。

目标网络是另一个稳定器。DQN的损失函数是:

Loss = (r + gamma * max_a' Q_target(s', a') - Q_online(s, a))^2

注意这里计算目标值时用的是Q_target网络,而不是正在更新的Q_online网络。因为如果用同一个网络同时计算预测值和目标值,每一轮更新都会让目标和预测一起动,优化过程就会变成"追一个不断移动的靶子",很容易震荡甚至发散。目标网络的做法是定期(比如每500步)把在线网络的参数复制过来,固定一段时间,让目标值相对稳定,训练才能收敛。

4.4 源码目录结构

项目源码的组织方式尽可能清晰,核心目录如下:

project/ ├── env/ │ ├── config.sumocfg # SUMO仿真配置 │ ├── net.net.xml # 路网文件 │ └── rou.xml # 车流文件 ├── agent/ │ ├── dqn.py # DQN网络定义 │ ├── replay_buffer.py # 经验回放缓冲区 │ └── train.py # 训练主循环 ├── utils/ │ ├── state_extractor.py # 从TraCI获取状态与奖励 │ └── config.py # 超参数配置 └── evaluate.py # 训练后评测脚本

源码本身并不复杂,但把它拆成环境、算法、工具三个模块,会让你后续扩展起来非常舒服。比如想换成PPO算法,只需要替换agent目录里的文件;想换成多路口场景,只需要在utils里增加一个状态聚合器。

5. 实验结果对比与参数调优:DQN比固定配时强在哪

5.1 评价指标与固定配时基线

训练完成后,需要用一套客观指标来评估DQN到底有没有用。我做了两轮实验,第一轮把DQN和固定60秒周期配时做了对比,第二轮尝试了不同的车流强度。评测用的核心指标有三个:

  • 平均等待时间:所有车辆在路口前停车的平均总时长;
  • 平均排队长度:每个决策时刻各进口道排队的车辆数均值;
  • 路网吞吐量:仿真时段内通过路口的车辆总数。

固定配时的基线直接用SUMO默认信号机方案,不做任何优化。评测时把DQN的信号灯控制程序接到同一套路网和车流文件上,其他条件完全一致,只让信号灯决策逻辑不同。

5.2 训练曲线怎么解读

DQN训练初期通常会有一段"表现很差"的阶段。我的经验里,前20个episode奖励值甚至明显低于固定配时的对照值,因为epsilon很大,智能体在疯狂探索,经常做出不合常理的切换动作,导致路口频繁出现"放行空车道、堵住车流量大的方向"这种尴尬局面。

这其实是正常现象,不要慌。随着训练进行,epsilon逐渐衰减,经验池里累积了足够的有效样本,网络开始学到"排队长的方向优先放行"这类规律,奖励曲线会逐步上升并超过基线。大概训练到150个episode左右,奖励曲线变得平稳,这时再跑评测,DQN的每个指标都会有可观提升。

我在一次典型的对比实验里得到的数字大概是:在中等流量下,DQN比固定配时降低了约18%的平均等待时间和22%的平均排队长度;在轻流量场景下两者差别不大,因为车本来就不堵;在重流量或流量突变场景下,DQN的优势会进一步拉开,有时等待时间能降低30%以上。这说明DQN强项在于应对不均衡、不稳定的车流,而不是替代所有配时方案。

5.3 影响收敛的关键参数

训练过程中有几处参数是真正决定成败的,我单独拿出来说。

第一个是奖励的尺度。如果奖励数值波动太大,比如排队长度变化从-30到+30,网络会对梯度方向非常敏感,必须把奖励除一个缩放因子(我这里除以了最大排队长度,让奖励落在[-1,1]区间),训练稳定很多。

第二个是决策间隔。10秒是我最后选定的值,但建议你做一次敏感性分析。间隔太短,信号灯频繁变动,车流根本来不及响应;间隔太长,智能体在两次决策之间会错失很多优化机会。用不同流量脚本做几组对照,你会找到最适合自己场景的数字。

第三个是目标网络同步步数。设成太小,比如每100步同步一次,目标网络几乎等于在线网络,失去意义;设成太大,比如每5000步,目标值和当前预测偏离过大,训练早期容易不稳定。500到1000步是一个经验合理区间。

6. 踩坑记录与个人心得:十几个小时仿真换来的经验

6.1 相位切换导致的"处处红灯"问题

我第一次把"切换"动作设为直接调用traci.trafficlight.setPhase跳转到下一个绿灯相位,训练出来的效果惨不忍睹:路口经常出现四个方向全是红灯的状态,车辆全部停摆。原因是SUMO信号灯内部是有相位顺序的,直接跳转跳过了黄灯过渡阶段,破坏了信号灯状态机的内部一致性。

后来我把切换逻辑改成:设置当前相位剩余时长为极短值(1秒),让信号灯按SUMO自己的规则进入黄灯过渡相位,再进入下一个相位,问题就消失了。这里的一个经验是:不要跟仿真器内置的状态机对抗,尽量顺着它的机制去做控制,否则你会在很多莫名其妙的仿真异常上浪费时间。

6.2 奖励函数方差过大导致训练崩溃

还有一个坑出现在换用"平均等待时间变化量"作为奖励时。等待时间受单辆车极端值影响很大,偶尔一辆车等了3分钟,这个变量的变动会让奖励瞬间产生很大波动,DQN的损失函数跟着震荡,训练曲线一路发散。

后来我把奖励改成排队长度变化量,再乘一个缩放系数,训练曲线立刻稳定下来了。如果你的任务必须用等待时间做奖励,我建议对单值进行截断处理,比如最大等待时间封顶180秒,或者对奖励做clip到[-1,1],总之不要让极端值主导梯度方向。

6.3 如果打算扩展到多路口,该怎么做

单路口跑通之后,很多人会想扩展到多路口协调控制。我的建议是:不要简单粗暴地把每个路口都放一个独立DQN,那样多个智能体在共同环境中各自优化,容易出现震荡。更稳妥的方式是先做一个多路口共享参数的单智能体方案,把所有路口的状态拼接成一个大向量输入同一个网络,输出所有路口的动作,这样训练稳定且代码改动不大。再往上的图神经网络或多智能体强化学习属于进阶方向,建议先把单路口基本功打扎实再碰。

最后再分享一个小技巧:训练时每隔几个episode就把当前模型保存一份,同时跑一次固定配时基线作为对照。这样你能在训练过程中实时观察智能体是否真的超过了基线,也能在崩溃时回滚到之前效果最好的模型。我在这个项目里前前后后跑了十几个小时的仿真,大部分时间都花在调参和修bug上,只有把环境、建模、训练链路都理顺了,强化学习在信号灯上的效果才真正凸显出来。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 1:07:45

酒吧点餐小程序系统开发实战:从需求分析到上线部署指南

酒吧点餐小程序系统开发实战&#xff1a;从需求分析到上线部署指南 酒吧点餐小程序系统开发的核心在于将传统酒馆服务流程数字化&#xff0c;覆盖扫码点餐、桌位管理、会员营销与互动娱乐等场景&#xff0c;本质上是一套多端协同的移动门店解决方案。本文从需求分析、技术选型、…

作者头像 李华
网站建设 2026/9/9 1:04:03

锂离子电池P2D模型MATLAB实现:从电化学原理到仿真实战

简介&#xff1a;一套基于P2D模型的MATLAB仿真程序包&#xff0c;即Doyle-Fuller-Newman模型的数值实现&#xff0c;面向电池技术研究人员、电化学工程师及相关专业学生&#xff0c;可用于模拟锂离子电池内部的电化学过程&#xff0c;解决等效电路模型难以描述的微观机理问题。…

作者头像 李华
网站建设 2026/9/9 0:53:54

棉花目标检测数据集:YOLOv8工业级训练实战指南

简介&#xff1a;本资源是面向农业AI与计算机视觉研究者的棉花开花程度识别专用目标检测数据集&#xff0c;适用于YOLO系列、Faster R-CNN、SSD等主流模型的训练与验证&#xff0c;聚焦病害早期识别与健康状态判别等实际农情监测场景。数据集共13765张高质量田间图像&#xff0…

作者头像 李华
网站建设 2026/9/9 0:53:51

KAN网络在轴承故障诊断中的应用:从原理到实践

简介&#xff1a;基于KAN&#xff08;Kolmogorov–Arnold Networks&#xff09;的轴承故障诊断完整工程&#xff0c;面向深度学习、机械故障诊断方向的开发者与学生。项目使用Pytorch 2.2.2实现KAN网络&#xff0c;将可学习的样条激活函数直接置于权重上&#xff0c;替代传统ML…

作者头像 李华
网站建设 2026/9/9 0:50:37

DeepSeek Harness通用设置与Agent预设配置详解

1. 开篇&#xff1a;为什么通用设置和Agent预设这么重要DeepSeek Harness 装了、跑起来了、也能和模型说上话了&#xff0c;但离"真正用起来"还有一段距离。我自己的体会是&#xff0c;第一次跑通一个Agent demo只需要半小时&#xff0c;但要把Agent调得符合自己的使…

作者头像 李华
网站建设 2026/9/9 0:48:50

粒子群与模拟退火混合算法:原理、代码与调参实战

简介&#xff1a;这份PSO与SA组合算法源码包面向智能优化方向的研究者、算法初学者及MATLAB用户&#xff0c;针对粒子群算法易陷局部最优、模拟退火后期收敛缓慢等单算法短板&#xff0c;提供一套混合优化实现。压缩包内共有9个文件&#xff0c;其中5个为MATLAB脚本&#xff0c…

作者头像 李华