简介:压缩包内是一套模拟市政务中心排队叫号服务系统的Python源码示例,面向初、中级Python开发者及有业务流程仿真需求的学生,用简洁代码演示取号、排队、叫号的完整流程。包内共2个文件,包含1个Python主程序脚本与1个txt使用说明文档,整体大小仅1KB,结构轻量,无需第三方库即可运行。源码利用Python列表或队列结构维护号码顺序,通过循环与条件分支模拟窗口调度,体现了FIFO先来先服务原则与自动化处理思路,并预留有扩展注释,便于增加预约登记、等待统计等功能。该资源目前已有216人浏览学习,虽然体积小巧,但作为入门级模拟项目仍具有参考价值。研读示例,可以直观理解真实业务规则到程序逻辑的映射过程,掌握小型自动化系统的基础搭建方法,并为后续探索网络爬虫、数据分析等方向积累Python实践能力。
1. 模拟市政务中心排队叫号服务系统:为什么一个 Python 示例源码项目值得认真拆一遍
如果你在某个政务大厅站过二十分钟以上,大概率见过这样的场面:取号机前排着长队,窗口屏幕上的号码跳得极快,广播里喊的号却和屏幕对不上,过号的群众和窗口工作人员来回扯皮。模拟市政务中心排队叫号服务系统这类 Python 源码示例项目,就是把这一整套乱象背后的调度逻辑塞进一个可运行的 demo 里:取号、叫号、过号、重呼、多窗口分流、优先队列,最后再用大屏或控制台实时展示状态。对正在做课程设计、综合实训,或者需要给真实对接方案做原型验证的开发者来说,这套东西的价值不在“能跑”,而在它能帮你把队列状态机、并发锁、超时策略这些真实系统里最容易翻车的环节一次想清楚。核心难点从来不是界面漂不漂亮,而是同一个号码被两个窗口同时叫到、过号之后队列顺序被打乱这类黑匣子问题,要不要踩一遍,完全取决于你建模时怎么设计。
2. 先建模再写代码:排队叫号系统的状态机与队列选型
2.1 一个号码从取号到办结的五种状态,先画清楚再动手
我在接这类排队叫号项目时,第一步从来不写 Python,而是先在纸上把号码的状态流转画出来。一个从取号机里出来的号码,生命周期里无非五种状态:等待中(waiting)、已呼叫(calling)、办理中(serving)、已办结(done)、已过号(expired)。过程看起来直接,但过号会打破线性流转:窗口叫了三遍没人来,号码进入 expired;后来群众又回来了,窗口可以把它重新激活,重新进队或直接安排到某个窗口。这个“重新激活”的动作,就是真实大厅里最容易出纠纷的环节,也是模拟系统里最容易写错逻辑的地方。
我一般把状态机的实现拆成两部分:状态字段和状态迁移方法。状态字段就是一个字符串常量,放在 Ticket 对象上;状态迁移则强制走统一方法,不允许在业务代码里直接改 state。这样做的原因是,后续所有并发操作都在改 state,如果不统一出口,一旦出现“一个号码同时被叫号和过号”这种竞争条件,你根本不知道哪个代码路径把它改坏的。
常见做法是给五种状态各配一个标记位或枚举常量,然后在迁移方法里做合法性校验。比如叫号时只有 waiting 状态的号码能进 calling;进入 calling 后如果超时未到,只能从 calling 迁到 expired,而不能从 waiting 直接跳到 expired。这样看起来约束多了,但在排查并发问题时,日志里每一行状态变更都能对应到明确的代码路径,省下的时间远超写校验的那几分钟。
| 状态 | 触发动作 | 可迁移目标 | 说明 |
|---|---|---|---|
| waiting | 取号入队 | calling / expired | 排队中的正常状态 |
| calling | 窗口叫号 | serving / expired / waiting | 叫号后限时等待 |
| serving | 群众到窗办理 | done | 办理开始后不可中断 |
| done | 办理完成 | 无 | 终态 |
| expired | 叫号超时未到 | waiting / serving | 可被重新激活 |
2.2 队列选型:用 list 做队列是新手最常见的性能与逻辑双重陷阱
选队列结构时,很多人第一反应是用 list,往尾部 append,取头部 pop(0)。这在号码量小的时候看不出问题,但模拟政务中心大厅动辄一天上千个号,list 的 pop(0) 是 O(n) 操作,每次出队都要把后面所有元素往前挪一位。更隐蔽的问题是,list 的 pop(0) 在并发环境下的原子性没有任何保障,多线程窗口同时取号时,你很难判断谁先谁后。
我一般用 collections.deque,双端队列的两端操作都是 O(1)。但光有 deque 还不够:政务中心不是单一队列模型,而是按业务类型拆成多个队列。比如房产过户、社保咨询、税务综合是三个不同业务,每个业务绑定不同的窗口组。如果所有业务塞进一个队列,就会出现懂房产的窗口被分配到社保号,窗口无法办理,群众空等,秩序全乱。
所以队列结构我做成“业务类型 → 队列”的字典映射,每个业务队列内部再拆成两个 deque:普通队列和优先队列。优先队列服务老弱病残孕等需要照顾的群体,取号时有标识,叫号时优先队列非空就优先从里面取。这里有个常见误用:直接拿 Python 的 queue.PriorityQueue 做优先队列。PriorityQueue 适合按全局优先级排序的场景,但在政务叫号场景里,你既要有先到先得的公平性,又要支持优先插队,用两个普通 deque 分开放,比用优先级队列在逻辑上直观得多,也更容易在日志里确认“这次叫号是从哪个队列取的”。
3. 用 Python 跑通取号、叫号、过号:核心代码骨架与参数说明
3.1 先写 Ticket 和业务队列:给号码一个身份和归属
代码的起点是 Ticket 对象和一个带锁的业务队列。Ticket 记录号码序号、业务类型、优先级标记、创建时间和当前状态;业务队列负责生成号码、入队、出队,并用线程锁保护所有修改操作。下面这段代码是这类系统的常见骨架,我把队列核心逻辑写在 BusinessQueue 里。
# core/queue_model.py import threading import itertools import time from collections import deque class Ticket: """一个排队号码的完整状态载体""" def __init__(self, seq, biz_type, priority=False): self.seq = seq # 号码序号,如 12 self.biz_type = biz_type # 业务类型,如 "real_estate" self.priority = priority # 是否优先队列 self.state = "waiting" # waiting / calling / serving / done / expired self.created_at = time.time() self.called_at = None # 最近一次叫号时间 def __repr__(self): return f"<Ticket {self.biz_type}-{self.seq:03d} {self.state}>" class BusinessQueue: """单个业务类型的队列,内部拆普通与优先两个双端队列""" def __init__(self, biz_type, window_ids): self.biz_type = biz_type self.window_ids = window_ids # 可办理该业务的窗口编号列表 self._normal = deque() # 普通队列 self._priority = deque() # 优先队列 self._lock = threading.Lock() self._counter = itertools.count(1) # 号码从 1 开始递增 self.total_taken = 0 # 累计取号数 def take_ticket(self, priority=False): """取号:从计数器拿号,进入对应队列""" with self._lock: seq = next(self._counter) ticket = Ticket(seq, self.biz_type, priority) if priority: self._priority.append(ticket) else: self._normal.append(ticket) self.total_taken += 1 return ticket def next_ticket(self): """叫号:优先队列非空先出,否则普通队列出队""" with self._lock: if self._priority: ticket = self._priority.popleft() elif self._normal: ticket = self._normal.popleft() else: return None ticket.state = "calling" ticket.called_at = time.time() return ticket def requeue(self, ticket): """过号重排:回到普通队列末尾,状态重置为 waiting""" with self._lock: ticket.state = "waiting" self._normal.append(ticket)逻辑说明:take_ticket 负责生成号码并入队,next_ticket 负责叫号,requeue 负责过号后重新排队。这里刻意把普通和优先拆成两个 deque,而不是在 Ticket 上打一个优先级标记后全塞进同一个队列,目的是让 next_ticket 的出队逻辑保持“先看优先、再看普通”的清晰顺序,日志里也一眼能看到每次叫号来自哪条队列。
关键参数是两个:window_ids 是窗口与业务的绑定关系,决定这个业务能被哪些窗口叫号;count 从 1 起的号码序号自增,模拟真实取号机里的连续号码。如果你要模拟多个取号机并发取号,需要注意这里用的是进程内计数器,所有线程共享同一个 itertools.count,因此不会出现两个线程拿到同一个号。
3.2 窗口线程的叫号循环:超时、过号与重呼
队列有了,窗口侧需要一个循环来叫号。真实大厅里,窗口工作人员按一下按钮叫下一个号;模拟系统里,我给每个窗口开一个线程,循环执行“从队列取号 → 播报 → 等待群众到窗 → 办理或超时”这一整套流程。核心循环写在 WindowWorker 里。
# core/worker.py import threading import time class WindowWorker: """模拟一个窗口的工作人员,循环叫号与办理""" def __init__(self, window_id, biz_queue, timeout=30): self.window_id = window_id self.biz_queue = biz_queue # 绑定某个业务队列 self.timeout = timeout # 叫号后等待群众到窗的秒数 self.current_ticket = None self._stop = False def run(self): """窗口主循环:取号、等待、办理或过号""" while not self._stop: ticket = self.biz_queue.next_ticket() if ticket is None: # 队列为空,歇一会儿再试 time.sleep(1) continue print(f"[窗口{self.window_id}] 请 {ticket.biz_type}-{ticket.seq:03d} 号到 {self.window_id} 号窗口") arrived = self._wait_arrive() if arrived: ticket.state = "serving" print(f"[窗口{self.window_id}] {ticket.seq:03d} 号开始办理") self._handle_business() ticket.state = "done" print(f"[窗口{self.window_id}] {ticket.seq:03d} 号办结") else: print(f"[窗口{self.window_id}] {ticket.seq:03d} 号超时未到,标记过号") self.biz_queue.requeue(ticket) self.current_ticket = None def _wait_arrive(self): """模拟等待群众到窗:超时返回 False,代表过号""" waited = 0 while waited < self.timeout: time.sleep(1) waited += 1 # 这里可扩展为监听“到窗按钮”事件,demo 简化成固定超时 return False def _handle_business(self): """模拟办理耗时""" time.sleep(5) def stop(self): self._stop = True逻辑说明:WindowWorker 是一个典型的生产者-消费者模式中的消费者,它不断从业务队列里取号。_wait_arrive 是模拟信号等待的简化实现,真实系统里这里会监听一个“群众到窗”的输入事件,而不是固定 sleep。demo 里固定超时 30 秒是为了让过号逻辑能被触发,方便测试。timeout 是核心参数,政务中心通常设置在 30 到 60 秒之间;调太短容易误伤取杯水就回来的群众,调太长则窗口空转严重。
这里有个刻意的设计:超时之后调用 requeue 把号码放回普通队列末尾,而不是直接丢弃。重呼逻辑在代码里不单独做,是因为叫号后群众如果又回来了,窗口可以在当前业务办理间隙再调一次 next_ticket 试试——这个动作利用的是队列自身机制,不需要引入额外状态。如果你想做更精细的“重呼次数限制”,可以在 Ticket 上加一个 recall_count 字段,超过两次就真过号。
3.3 把系统拼起来:取号机线程与主控制台
窗口线程跑起来了,还得有人取号。真实系统里取号机是独立设备,模拟系统里我一般用两个线程模拟两台取号机,随机按业务和优先级生成号码。主线程则打印当前各队列长度和最近叫号记录,充当简化版的大屏。
# main.py import threading import random import time from core.queue_model import BusinessQueue from core.worker import WindowWorker def ticket_machine(biz_queues, stop_event): """模拟取号机:持续随机取号,随机标记优先""" biz_types = list(biz_queues.keys()) while not stop_event.is_set(): biz = random.choice(biz_types) priority = random.random() < 0.05 # 5% 概率为优先号 ticket = biz_queues[biz].take_ticket(priority) print(f"[取号机] {ticket} 出号") time.sleep(random.uniform(1.0, 3.0)) def main(): # 初始化三个业务队列,每个业务绑定两个窗口 biz_config = { "real_estate": ["W01", "W02"], "social_security": ["W03", "W04"], "tax": ["W05", "W06"], } biz_queues = { biz: BusinessQueue(biz, windows) for biz, windows in biz_config.items() } # 启动窗口线程 workers = [] for biz, windows in biz_config.items(): for wid in windows: worker = WindowWorker(wid, biz_queues[biz], timeout=30) workers.append(worker) threading.Thread(target=worker.run, daemon=True).start() # 启动取号机线程 stop_event = threading.Event() threading.Thread(target=ticket_machine, args=(biz_queues, stop_event), daemon=True).start() # 主线程充当简易监控大屏 try: while True: time.sleep(5) for biz, q in biz_queues.items(): print(f"[状态] {biz}: 普通队列 {len(q._normal)} 人, 优先队列 {len(q._priority)} 人") except KeyboardInterrupt: stop_event.set() print("系统停止") if __name__ == "__main__": main()逻辑说明:main 函数里首先建立业务与窗口的绑定关系,每个业务对应两个窗口,然后按这个配置启动窗口线程和取号机线程。主线程每 5 秒打印一次队列长度。取号机线程的 sleep 间隔决定了号码生成速度,1 到 3 秒的随机间隔用来模拟高峰和平峰的差异;5% 的优先级概率是按政务中心常见比例设的,你可以按实际场景调。
运行这个 demo 可以直接看到完整循环:取号机不断出号,窗口随机叫号,队列长度实时变化,偶尔有号码超时被放回队尾。这里还没有涉及并发安全的核心问题——多窗口同时取号时会不会拿重号,窗口叫号时队列被并发修改会不会崩,这是下一章要解决的事。
4. 并发与实时展示:让模拟系统像真实大厅一样工作
4.1 多线程窗口模型的锁设计:一个锁还是多个锁
上一章的代码里,BusinessQueue 内部只维护了一把 self._lock,所有对队列的读写都经过它。这个设计在号码量不大时没问题,但政务中心高峰期队列操作非常频繁,单锁会让取号机和所有窗口在锁上互相等待,吞吐量上不去。常见做法是按队列加锁,而不是全局一把锁:每个业务队列一把锁,不同业务的取号和叫号互不干扰。
更细一点,可以把锁拆成队列结构锁和状态锁。实际操作中我一般不过度设计:每个 BusinessQueue 维护 self._lock 已经足够保证 deque 操作的原子性,因为最危险的竞争只发生在同一个业务的队列上。跨业务之间即使同时操作,也没有共享可变状态,不需要加锁。下面的改造给每个业务队列独立配置锁,并把锁的获取范围压缩到最小。
# core/queue_model.py 中调整锁粒度 import threading class BusinessQueue: """按业务队列独立加锁,减少锁竞争""" def __init__(self, biz_type, window_ids): self.biz_type = biz_type self.window_ids = window_ids self._normal = deque() self._priority = deque() self._lock = threading.Lock() # 本队列一把锁 self._counter = itertools.count(1) def next_ticket(self): with self._lock: if self._priority: ticket = self._priority.popleft() elif self._normal: ticket = self._normal.popleft() else: return None ticket.state = "calling" ticket.called_at = time.time() return ticket参数说明:这里选择“每队列一锁”而不是“全局一锁”,是让不同业务的取号并发执行。如果模拟下午三点的高峰,三台取号机同时为三个业务取号,全局锁会让它们串行等待;独立锁则三个业务可并行。需要特别注意:不要在持锁状态下执行耗时操作,比如调用 time.sleep 或打印日志。上面 console print 放在锁外,就是为了避免窗口慢速打印阻塞整个队列的其他操作。
4.2 大屏与语音播报:模块解耦,别把 UI 塞进队列逻辑
很多初学者会把大屏刷新代码直接写在队列操作里,比如在 next_ticket 里 print 一行叫号信息。这在 demo 阶段没问题,一旦加了大屏刷新、语音播报,队列性能立刻被拖垮。因为打印和播报都是相对耗时的 I/O,而锁内 I/O 会让其他线程全部堵在锁上。我一般把大屏展示做成独立的订阅模式:队列操作只负责改状态,大屏组件通过监听状态变化来刷新展示。
# ui/board.py from collections import defaultdict class DisplayBoard: """模拟大厅叫号大屏,订阅号码状态变化""" def __init__(self): self._listeners = [] def subscribe(self, fn): self._listeners.append(fn) def publish(self, ticket): for fn in self._listeners: fn(ticket) # 主程序中组合 board = DisplayBoard() board.subscribe(lambda t: print(f"[大屏] 当前叫号: {t.biz_type}-{t.seq:03d} 请到 {t.window_id} 窗口"))语音播报同理,Windows 下按常见做法可用 winsound 播放预设 WAV 文件,Linux 下用系统 TTS 命令。代码上只需要在 notify 回调里加一行调用,不需要改队列核心。这样的好处是:队列逻辑可以在无 UI 的纯 Python 环境下跑单元测试,UI 和播报只是挂在外面的监听者。这是整个模拟系统里最值得坚持的结构决策,它让你后续接真实硬件或者换 Web 大屏时,核心调度代码一行都不用动。
4.3 模拟系统对接真实服务总线的可行路径
到这里,demo 基本是一个完整的单机模拟系统了。如果你要把它往前再推一步——比如给真实政务中心做原型验证——常见做法是把队列管理器抽成独立服务,用消息队列或 HTTP 接口对外提供取号和叫号能力。
我在原型项目里比较常用的方案是:保留 BusinessQueue 核心不动,外面套一层 Flask 或 FastAPI,暴露三个接口:POST /take(取号)、POST /call(叫号)、GET /queue_status(队列状态)。这样取号机变成一台真能扫码出票的设备,窗口叫号器变成运行在 Windows 小主机上的客户端。多进程部署时要注意:进程内 threading.Lock 只保护单进程内的线程,跨进程共享队列必须用 Redis 或 RabbitMQ 这类外部存储。很多人在这一步翻车,就是把单机 demo 直接推到多机,然后发现两个窗口进程各维护了一份队列,同一个号被叫了两次。
5. 排队叫号系统避坑:5 个典型翻车现场与排查思路
5.1 两个窗口同时叫到同一个号:锁范围没盖住状态变更
现象:同一时间两个窗口的屏幕上出现同一个号码,群众不知道该去哪个窗口,日志里也打印了两条相同号码的叫号记录。
原因:next_ticket 出队时拿了锁,但出队之后、设置 ticket.state = "calling" 之前,另一个线程可能已经通过其他路径改了同一个 ticket 的状态。更隐蔽的是,如果 Ticket 状态迁移方法里没有加锁,窗口 A 把它从 waiting 改成 calling,窗口 B 同时把它从 waiting 改成 expired,最后写回的状态取决于线程调度顺序,谁后写谁赢,数据就乱了。
解决:把状态变更收口到 BusinessQueue 的锁内,状态迁移方法内部再校验一遍当前状态是否允许迁移。比如只有 waiting 才能迁到 calling,只有 calling 才能迁到 expired。校验失败直接抛异常或记录日志,而不是静默覆盖。这个错误最坑的地方是偶发,不压测不出来,所以写代码时就该把“状态机合法性校验”当成和“取号出队”同等级的核心逻辑。
5.2 过号重呼导致队头饿死:每次都插队尾让重复过号的人永远排不上
现象:一个群众三次被叫号三次不在,每次都被放回队尾,而新号码又不断从队尾进来,他的号码永远轮不到。
原因:requeue 把过号 tickets 直接放回普通队列末尾,但如果这个人每次叫号时都不在,他就反复从队尾移动到队头再被叫到、再过号,循环往复。极端场景下,一个反复过号的号码会让队列形成“每次轮到他就跳过”的假象。
解决:给 Ticket 加 recall_count 字段,过号重排时如果从 waiting 到 calling 后又超时,次数加一;达到上限(比如 2 次)就不再入队,直接置为 expired,并打印“请到服务台人工处理”。真实大厅也是这么做的——过号超过一定次数后不再自动排队,改人工窗口处理。模拟系统里这一条能把复杂情况收敛,也能让队列的公平性可解释:不是无限插队,而是有限重试。
5.3 大屏刷新卡死,整个叫号系统停止响应
现象:启动系统后跑半小时,大屏偶尔卡住,随后所有窗口都不再叫号,控制台像是被冻结。
原因:大屏刷新用了 Tkinter 的 mainloop,但刷新逻辑放在了队列的操作线程里。Tkinter 的 UI 操作必须在主线程执行,其他线程直接操作 UI 控件会导致死锁或崩溃。更隐蔽的情况是,大屏刷新里做了耗时的查询或网络请求,把 UI 线程阻塞住,进而导致整个进程看起来像死了。
解决:所有 UI 刷新操作统一投递到主线程事件循环。Tkinter 里用 after 或队列方式传递刷新消息,不要在工作线程里直接改控件。如果只是控制台模拟,用 print 也必须在锁外。最简单的判断标准是:队列核心代码里除了 return 之外的任何 I/O,都需要仔细掂量是否值得拿锁。
5.4 优先队列太长,普通号码长时间不被叫号
现象:优先队列不断有号码进来,普通队列长度持续增长,普通号码等待时间越来越长,然后群众开始围住服务台。
原因:next_ticket 的逻辑是“优先队列非空就永远先出优先队列”,如果取号机生成的优先号比例过高或者优先号处理时间很长,普通队列就会饿死。政务中心如果某段时间老年证办理集中,这个现象很容易出现。
解决:给优先队列加一个限制——每连续叫 N 个优先号后,强制叫一个普通号。也可以在 BusinessQueue 里记录优先号连续叫号次数,当超过阈值时强制从普通队列取号。这个阈值要按实际业务配比调,我一般先设为 3:3 个优先号后 1 个普通号。这样既保证了优先群体不被长时间等待,也避免普通队列完全停滞。
5.5 模拟数据无法复现,压测结果对不上
现象:同一份代码跑两次压测,平均等待时间一次 22 秒一次 47 秒,完全没法判断优化是否有效。
原因:ticket_machine 里用了 random.uniform 控制取号间隔,每次运行的随机序列不同,导致号码到达模式不一致。压测结果差异可能来自测试数据本身,而不是代码改动。
解决:压测前给 random 模块设置固定种子,或者把取号时间序列写成配置文件,保证每次压测的到达间隔完全一致。常见做法是在 main 入口加一行 random.seed(42),这样所有随机行为可复现。严谨一点的做法是记录每一笔取号的时间戳到日志,之后用日志重放,这样连线程调度的影响都能对比。
6. 进阶验证与调试技巧:把队列核心压出真实瓶颈
队列叫号系统写出来能跑只是第一步,要确认它能应对高峰,你需要一套可重复的压测方法。我的习惯是给 QueueManager 写一个纯逻辑的压测入口,不启动窗口线程,只模拟一大波取号请求,然后统计每个号码从取号到被叫的平均等待时间、最长等待时间和队列最大深度。这三个指标直接对应政务服务中最敏感的“群众要等多久”。
# 压测示例:模拟 1000 个取号请求,测量等待分布 python scripts/stress_test.py --tickets 1000 --windows 3 --seed 42压测脚本里我会额外打印一个分位数——P95 等待时间,意思是 95% 的号码在多少秒内被叫到。这个指标比平均值更有意义:平均值会被少数超长等待拉高或拉低,而 P95 能暴露队头阻塞问题。我在某次改造里把队列从全业务共享改成按业务分队列后,平均值只降了 8%,P95 却降了 40%,这就是分位数的价值——你优化了对的方向。
另一个值得做的验证是日志重放。在 BusinessQueue 的 take_ticket 和 next_ticket 里各加一行 JSON 日志,记录时间戳、号码、动作和当前队列长度。压测跑完后,把日志按时间排序,你可以看到完整的号码流转过程。排查“某个号码消失”时,直接从日志里 grep 这个号码的所有动作,看它是取号后没被叫,还是被叫后状态没更新。这一招比在代码里加 print 高效得多,因为打印在并发下是乱的,而日志有顺序、有时间戳。
状态不变量检查是我最近才养成的习惯,但它确实救过我一次。压测结束后,遍历当天所有 Ticket,断言每个号码的状态机迁移路径合法:waiting 后面只能是 calling 或 expired,calling 后面只能是 serving 或 expired,serving 后面只能是 done。如果有号码的 state 是 calling 但记录里没有对应的 done 或 expired,说明有线程在状态变更时丢失了更新。这种问题用黑匣子式的功能测试永远测不出来,只有明确的状态不变量检查才能拦住。把这段断言写进 test 脚本,后续每次改动队列逻辑后跑一遍,比任何人工回归都可靠。
最后分享一个我自己的教训:有一次赶进度,为了演示效果,我给这个模拟系统加了非常花哨的 Web 前端,结果调试 UI 的时间比写队列逻辑多了一倍。后来我把前端整个拆掉,用最简单的控制台界面验证核心调度,验证通过之后才回头看展示层。这个习惯我一直保留到现在:核心调度永远优先做厚,展示层永远最后加。希望帮到你。
本文还有配套的精品资源,点击获取