第 32 天:快速回覆 QuickReply 介绍
对其他人来说也许没什么,但对他而言这可真是不容易。因为这个男人认为,打从他有记忆以来就这么相信,某部分的他是用快速回覆按钮 (QuickReplyButton) 做出来的。他唯恐一个错误的回发动作 (PostbackAction) 被触发,然后他将会在她面前分崩离析。
~节录自《聊天机器人的历史:我妈的忧伤》
延伸自系列文 《从LINE BOT到资料视觉化:赖田捕手》
《赖田捕手:追加篇》:
第 31 天:初始化 LINE BOT on Heroku第 32 天:快速回覆 QuickReply 介绍第 33 天:妥善运用 Heroku APP 暂存空间第 34 天:妥善运用 LINE Notify 免费推播第 35 天:製造 Deploy to Heroku 按钮根据可靠的?科学研究,草泥马是一群视觉系的动物。草泥马心理分析权威是这么说的:「良好而适当的视觉刺激具有稳定草泥马情绪波动,促进正向思考的能力,是培育积极且强韧的草泥马的不二法门」。这应该是个生理影响心理,而后心理又影响生理的最佳例子。那么这就让人好奇了,到底什么是良好而适当的视觉刺激呢?答案是,草泥马们相当享受观赏双色打光影像的时刻。
所谓的双色打光,顾名思义,就是在一个主要观赏目标的两侧,分别打上两种不同的单色光,如图一。
图一、双色打光!
身为一个专业的草泥马训练师,我当然义不容辞要为我所饲养的草泥马们打造出最舒适的环境,也就是将所有的图片转为双色打光的图片,供草泥马们惬意的欣赏。这件事说难不难,说简单,好像也没那么容易。要将所有的图片转为双色打光的图片?那我岂不是一整天忙着修图就饱了,这样根本没时间照顾草泥马啊。幸好我是一个懂得写 LINE BOT 的草泥马训练师。是的,只要写出一个专门将一般图片转为双色打光图片的 LINE BOT,那所有的任务都交给 LINE BOT 就搞定了。是不是很吸引人呢?那么,接下来请容我娓娓道来,我是如何建构出一个擅长双色打光的 LINE BOT。
参考资料
关于双色打光的概念,鼓励各位读者参考 Logos By Nick 的影片分享。相当感谢他的热情教学,才有我的双色打光 LINE BOT。
再整理资料夹
工欲善其事,必先利其器。为了完成我们了不起的双色打光 LINE BOT,将程式码好好的分门别类是一件相当重要的工作。在第 31 天当中,我们已经将程式码做了一个大略的分类,把初始化 LINE BOT 的程式码、处理handler
相关的程式码、处理route
相关的程式码拆开变成了三个档案。档案结构如下:
D:\appendix>tree /fFolder PATH listingVolume serial number is 9C33-6XXDD:.│ runtime.txt│ requirements.txt│ Procfile│ Alma.py│└───app __init__.py models_for_line.py routes.py
为了要做出功能强大的 LINE BOT,我们会在处理handler
相关的任务中写下更多更长更繁琐的程式码。为了保持乾净的程式码,便于继续扩充和维护,我打算把models_for_line.py
这个档案中的详细功能拉出来,另外创立几个档案,因此资料夹的档案结构会变成这样:
D:\appendix>tree /fFolder PATH listingVolume serial number is 9C33-6XXDD:.│ runtime.txt│ requirements.txt│ Procfile│ Alma.py│└───app │ __init__.py │ models_for_line.py │ routes.py │ └───custom_models AlmaTalks.py
详细回覆使用者的方法被放到app/custom_models/AlmaTalks.py
:
app/custom_models/AlmaTalks.py
from app import line_bot_apifrom linebot.models import TextSendMessagedef default_reply(event): name = line_bot_api.get_profile(event.source.user_id).display_name line_bot_api.reply_message( event.reply_token, TextSendMessage(text=f"Hello {name}!") )
而原本的models_for_line.py
则剩下:
app/models_for_line.py
from app import handlerfrom app.custom_models import AlmaTalksfrom linebot.models import MessageEvent, TextMessage@handler.add(MessageEvent, message=TextMessage)def handle_message(event): AlmaTalks.default_reply(event)
为什么选择这么做呢?因为我希望models_for_line.py
这个档案简单一点,让人一目了然。而真正困难而仔细的各种任务就放到app/custom_models
底下的不同档案里。
利用 PIL 以及 numpy 完成双色打光
那么,实际上最核心的双色打光程式码可以怎么做呢?概念上来说,应该也不难:
创造一个跟原始图像大小一样的双色渐层图像将双色渐层图像与原始图像叠图两步骤完成!
而 Python 在影像处理以及数据处理方面有非常丰富的资源库,我们可以简单的用 PIL 和 numpy 这两个资源库来完成这个任务。
import numpy as npfrom PIL import Imagefrom PIL import ImageDrawdef sigmoid(x, alpha): return 1 /(1 + np.exp(-x * alpha))def create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor): layer_gradient = Image.new('RGB', layer_im.size) draw = ImageDraw.Draw(layer_gradient) for i in range(layer_im.size[0]): value = sigmoid(i - layer_im.size[0] / 2, gradient_factor / layer_im.size[0]) fill_color = np.array(first_tone) * value + np.array(second_tone) * (1 - value) draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int'))) return layer_gradient
所谓的渐层,即是色彩由深而浅的变化,于是我们可以先创造一个sigmoid
函式来模拟这种变化,并且设计两个参数,x
和alpha
,x
与位置有关,根据位置来做出不同颜色的深浅变化。而第二个参数alpha
则让我们可以控制渐层变化的幅度。以我们设计的sigmoid
函式来说,alpha
越小,梯度变化越平缓,而alpha
越大,梯度变化越急遽,具体结果可以用matplotlib
这个用来作图的资源库来检视 (图二):
import matplotlib.pyplot as pltfig, axes = plt.subplots(3, 1, figsize=(10, 10))x = np.linspace(0, 10, 101)for ax, g in zip(axes.flatten(), [1, 10, 100]): ax.plot(x, sigmoid(x-5, g), label=f'gradient={g}') ax.legend(fontsize='x-large')
图二、sigmoid
函式运作方式
在create_gradient_layer
这个函式里,我们先用Image.new()
来产生一张新的图像,接着再用ImageDraw.Draw()
为这张新的图像上色。
layer_gradient = Image.new('RGB', layer_im.size)
:
产生一张以'RGB'
做为资料储存格式的图像layer_gradient
,图像大小则参考原始图像layer_im.size
。
draw = ImageDraw.Draw(layer_gradient)
:
準备在新的图像layer_gradient
上面作画。
draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int')))
:
从座标(i, 0)
开始到座标(i, layer_im.size[1]-1)
为止,画一条线,该线条的颜色则由fill=tuple(fill_color.astype('int'))
来定义。由于我们的layer_gradient
这个新图像是用'RGB'
做为资料储存的格式,因此fill
也要用相同的形式来表示,如红色应该表示为(255, 0, 0)
,绿色是(0, 255, 0)
,诸如此类。
这边假定有一张demo_image.jpg
,那么我们执行create_ gradient_layer
:
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 1)
图三、gradient_factor=1
创造出来的双色渐层图像
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 10)
图四、gradient_factor=10
创造出来的双色渐层图像
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 100)
图五、gradient_factor=100
创造出来的双色渐层图像
这边则可以用 PIL 提供的
blend
或是composite
两种方式将双色渐层图像与原始图像进行叠图。Image.blend
:Image.blend(layer_im, layer_gradient, alpha=0.5)
这边的alpha
可以调整两张影像是用何种比例进行叠图的。若希望第一张影像layer_im
所佔的比例高一些,则alpha
要小一点,若希望第二张影像layer_gradient
所佔的比例高一些,则alpha
就设定大一点。极端一点来说,若alpha=0
则会直接得到第一张影像layer_im
,而alpha=1
会直接得到第二张影像layer_gradient
。
图六、Image.blend
叠图
Image.composite
:# 用layer_im当滤镜,深色的地方双色打光效果明显Image.composite(layer_im, layer_gradient, layer_im.convert('L'))# 用ImageOps.invert(layer_im)当滤镜,浅色的地方双色打光效果明显Image.composite(layer_im, layer_gradient, ImageOps.invert(layer_im).convert('L'))
第二种方式composite
则提供了有趣的叠图选择。一样是将两张影像layer_im
和layer_gradient
叠在一起,不过第二张影像要透过一个带有透明讯息的滤镜来叠图。而最直觉的滤镜,就是layer_im.convert('L')
,这样可以做出如图七的影像,深色的地方会有较强的双色打光效果。
图七、Image.composite
叠图
另外我们也可以用ImageOps.invert
,将影像黑白反转,这样的滤镜可以做出如图八的影像,浅色的地方会有较强的双色打光效果。
图八、Image.composite
加上ImageOps.invert
叠图
完成之后,我们可以把所有程式码串在一起了:
import numpy as npfrom PIL import Imagefrom PIL import ImageDrawfrom PIL import ImageOpsdef sigmoid(x, alpha): return 1 /(1 + np.exp(-x * alpha))def create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor): layer_gradient = Image.new('RGB', layer_im.size) draw = ImageDraw.Draw(layer_gradient) for i in range(layer_im.size[0]): value = sigmoid(i - layer_im.size[0] / 2, gradient_factor / layer_im.size[0]) fill_color = np.array(first_tone) * value + np.array(second_tone) * (1 - value) draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int'))) return layer_gradientdef dual_tone_run(file, mode, gradient_factor, first_tone, second_tone): layer_im = Image.open(file).convert('RGBA') layer_gradient = create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor).convert('RGBA') if mode == 'blend': dual_tone = Image.blend(layer_im, layer_gradient, 0.5) elif mode == 'composite': dual_tone = Image.composite(layer_im, layer_gradient, layer_im.convert('L')) elif mode == 'composite_invert': dual_tone = Image.composite(layer_im, layer_gradient, ImageOps.invert(layer_im.convert('L')).convert('L')) return dual_tone
快速回覆
完成了双色打光的运作之后,就可以开始来设计我们的 LINE BOT 了。大家在实作双色打光的程式码时,应该注意到这段程式码其实拥有许多参数,而这些参数都可以大大的影响最后产生出来的图像。因此我们在建构这个影像处理 LINE BOT 的时候,其实是可以给出不少选项供使用者依照个人偏好或是情境做出选择的。而在这方面,LINE 相当贴心的推出了各种功能,让使用者可以更容易的与 LINE BOT 进行互动,像我们今天要介绍的快速回覆 (QuickReply
) 就是其中一种。
快速回覆有点像 LINE BOT 丢给使用者的选择题。LINE BOT 提供选项,而使用者从选择其中一个作为回答,回传给 LINE BOT,完成一次互动,操作起来简单明了。不过,快速回覆最大的缺点是只会显示在手机介面上。因此目前没办法在电脑版的介面上透过快速回覆与 LINT BOT 进行互动。
要使用快速回覆也不难,先看一段 LINE 官方给出的使用範例:
text_message = TextSendMessage( text='Hello, world', quick_reply=QuickReply(items=[ QuickReplyButton(action=MessageAction(label="label", text="text")) ]))
这段操作的结果是,LINE BOT 会传送文字讯息'Hello, world'
给使用者,而使用者会看到一个快速回覆按钮 (QuickReplyButton
)。如果我们想要提供更多的快速回覆按钮,可以这么做:
quick_reply=QuickReply( items=[ QuickReplyButton(), QuickReplyButton(), QuickReplyButton(), … ])
只要在代表选项的items
这个清单当中放入更多的QuickReplyButton
就可以了。
回到我们的範例。这边所提供的快速回覆按钮的文字显示为"label"
。使用者可以点击这个按钮,当作对 LINE BOT 的回答。点下去的时候,该按钮所设定的动作被触发,也就是讯息动作MessageAction(label="label", text="text")
,这时候系统会为使用者传送文字讯息"text"
给 LINE BOT。换句话说,点击带有MessageAction(label="label", text="text")
动作的按钮,就好像使用者亲自发送文字讯息"text"
给 LINE BOT 一样。
常用的几种动作包括MessageAction
、PostbackAction
、URIAction
等。回发动作PostbackAction
可以看做进阶的MessageAction
,除了可以发送文字讯息之外,还会多传送隐藏的资料 (不会显示在 LINE 的对话视窗当中),方便 LINE BOT 根据这些资料做出适当的回应。而URIAction
则是为使用者开启指定连结的动作。
这边我打算用回发动作PostbackAction
,使用範例如下:
action=PostbackAction( label='postback', display_text='postback text', data='action=buy&itemid=1' )
label='postback'
:
用来设定代表触发该动作的按钮的显示文字。
display_text='postback text'
:
触发该动作之后,系统为使用者传送的文字讯息。也就是说,点击此按钮,就好像使用者向 LINE BOT 传送了'postback text'
这样的文字讯息。但跟MessageAction
不同的是,LINE BOT 并不会因此收到MessageEvent
。相对的,LINE BOT 会收到的是回发事件PostbackEvent
,因为这可是PostbackAction
啊。所以说,跟MessageAction
不同,这边的文字讯息只是假的,是显示文字 (display_text
) 而已。
data='action=buy&itemid=1'
:
这个才是PostbackAction
真正传送到 LINE BOT 的资讯。LINE BOT 会收到PostbackEvent
,而我们可以藉由event.postback.data
来拿到这些资讯。
好的,既然我们已经知道怎么做出双色打光,也了解怎么使用QuickReply
,那现在就可以开始来规划一下整个 LINE BOT 跟使用者的互动流程,看看我们的 LINE BOT 怎么替使用者客製化作出双色打光影像处理。
这部分大家当然可以自由发挥,我就提一个简单的流程来说明我会如何架构:
图九、LINE BOT 与使用者互动流程
图九是我打算採用的流程概念图,整个互动从使用者向 LINE BOT 传送图像开始,也就是当 LINE BOT 接收到ImageEvent
,整段流程就开始了。使用者依序设定好模式 (mode)、梯度 (gradient_factor)、第一种颜色 (first_tone)、以及第二种颜色 (second_tone),接着 LINE BOT 就根据这些使用者给出的条件,去对使用者一开始传送过来的图像做影像处理。按照这个规划,我们就得为models_for_line.py
这个档案添加几段程式码:
app/models_for_line.py
from app import handlerfrom app.custom_models import AlmaTalksfrom linebot.models import ImageMessage, PostbackEvent@handler.add(MessageEvent, message=ImageMessage)def handle_image(event): AlmaTalks.phase_start(event)@handler.add(PostbackEvent)def handle_postback(event): if not event.postback.data.startswith('second_tone='): AlmaTalks.phase_intermediate(event) else: AlmaTalks.phase_finish(event)
按照这样的设计,当使用者向 LINE BOT 传送图片 (ImageMessage
),任务开始,启动AlmaTalks.phase_start
这个函式。接着 LINE BOT 会依序接收到使用者透过QuickReplyButton
传送过来的PostbackEvent
,而这些就交给AlmaTalks.phase_intermediate
和AlmaTalks.phase_finish
来处理。所以现在我们就要来着手撰写这几个函式。
app/custom_models/AlmaTalks.py
def phase_start(event): # 初始化表格 CallDatabase.init_table() # 检查使用者资料是否存在 if CallDatabase.check_record(event.source.user_id): _ = CallDatabase.update_record(event.source.user_id, 'message_id', event.message.id) else: _ = CallDatabase.init_record(event.source.user_id, event.message.id) mode_dict = {'blend': '线性叠图', 'composite': '滤镜叠图', 'composite_invert': '反式滤镜叠图'} line_bot_api.reply_message( event.reply_token, TextSendMessage( text=f"[1/4] 今晚,我想来点双色打光!\n请选择双色打光模式:", quick_reply=QuickReply( items=[QuickReplyButton(action=PostbackAction( label=v, display_text=f'打光模式:{v}', data=f'mode={k}')) for k, v in mode_dict.items() ] ) ) )
为了让 LINE BOT 能够记得使用者选择的设定,我们在第 31 天添加了一个扩充元件 Heroku Postgres 当作资料库,将我们需要保留的资料,也就是使用者的设定,储存起来。所有与资料库的互动,包括初始化表格、检查资料、放入资料、更新资料等等,我都打算放进另一个档案里,也就是app/custom_models/CallDatabase.py
,等等会再详细介绍。
根据这段程式码,函式phase_start
要做的事就是当接收到使用者传来的图片时,为使用者在资料库中的表格初始化一笔资料 (或是更新资料),接着透过QuickReplyButton
提供不同的打光模式选择给使用者。
app/custom_models/AlmaTalks.py
def phase_intermediate(event): color_dict = { 'red': '红', 'orange': '橙', 'yellow': '黄', 'green': '绿', 'blue': '蓝', 'purple': '紫' } reply_dict = { 'mode': '[2/4] 今晚,继续来点双色打光!\n请选择色彩变化梯度:', 'gradient_factor': '[3/4] 今晚,还想来点双色打光!\n请选择第一道色彩:', 'first_tone': '[4/4] 今晚,最后来点双色打光!\n请选择第二道色彩:' } quick_button_dict = { 'mode': [QuickReplyButton( action=PostbackAction( label=i, display_text=f'变化梯度:{i}', data=f'gradient_factor={i}')) for i in (5, 10, 50, 100) ], 'gradient_factor': [QuickReplyButton( action=PostbackAction( label=j, display_text=f'第一道色彩:{j}', data=f'first_tone={i}')) for i, j in color_dict.items() ], 'first_tone': [QuickReplyButton( action=PostbackAction( label=j, display_text=f'第二道色彩:{j}', data=f'second_tone={i}')) for i, j in color_dict.items() ] } user_id = event.source.user_id postback_data = event.postback.data current_phase = postback_data.split('=')[0] # 依照使用者的选择更新资料 CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1]) line_bot_api.reply_message( event.reply_token, TextSendMessage( text=reply_dict[current_phase], quick_reply=QuickReply( items=quick_button_dict[current_phase])) )
这边应该也难不倒大家。先建构好reply_dict
和quick_button_dict
,按照流程準备好不同阶段相对应的回答跟快速回覆按钮。这边我在QuickReplyButton
的PostbackAction
里藏了不同阶段的暗示,让 LINE BOT 在收到PostbackEvent
时,可以藉由event.postback.data
来判断这一连串的互动是进行到哪一阶段了。
app/custom_models/AlmaTalks.py
def phase_finish(event): user_id = event.source.user_id postback_data = event.postback.data current_phase = postback_data.split('=')[0] # 更新资料并取得最后的完整设定 record = CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1]) line_bot_api.reply_message( event.reply_token, TextSendMessage(text=str(record)) )
最后一个阶段,在使用者选择好第二道色彩的同时,我们让 LINE BOT 更新这一笔设定,并从资料库中提取该名使用者先前的所有设定,包括打光模式、变化梯度、以及选择的两种色彩。我们可以简单的用TextSendMessage
来将设定的内容传回给使用者,检查 LINE BOT 是否真的记下了这些内容。
连接 Heroku Postgres
在前面我们设计的互动过程中,LINE BOT 是利用app/custom_models/CallDatabase.py
来操作 Heroku Postgres,纪录、更新、提取使用者的设定。在互动流程大致底定之后,现在该是时候把CallDatabase
给生出来了。
app/custom_models/CallDatabase.py
import osimport psycopg2def access_database(): DATABASE_URL = os.environ['DATABASE_URL'] conn = psycopg2.connect(DATABASE_URL, sslmode='require') cursor = conn.cursor() return conn, cursordef init_table(): conn, cursor = access_database() postgres_table_query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'" cursor.execute(postgres_table_query) table_records = cursor.fetchall() table_records = [i[0] for i in table_records] if 'user_dualtone_settings' not in table_records: create_table_query = """CREATE TABLE user_dualtone_settings ( user_id VARCHAR ( 50 ) PRIMARY KEY, message_id VARCHAR ( 50 ) NOT NULL, mode VARCHAR ( 20 ) NOT NULL, gradient_factor VARCHAR ( 20 ) NOT NULL, first_tone VARCHAR ( 20 ) NOT NULL, second_tone VARCHAR ( 20 ) NOT NULL );""" cursor.execute(create_table_query) conn.commit() return True
先写好两个函式:连接 Heroku Postgres 资料库的起手式access_database
,以及在资料库中初始化表格init_table
。先跟大家道个歉。在第 31 天当中,我也写了一个init_table
这个函式,用来创造我们需要用的表格'user_dualtone_settings'
。不过经过一个星期的修订之后,我想要更改表格的栏位和资料类型,改动如下:
CREATE TABLE user_dualtone_settings ( user_id VARCHAR ( 50 ) PRIMARY KEY, message_id VARCHAR ( 50 ) NOT NULL, mode VARCHAR ( 20 ) NOT NULL, gradient_factor VARCHAR ( 20 ) NOT NULL, first_tone VARCHAR ( 20 ) NOT NULL, second_tone VARCHAR ( 20 ) NOT NULL);
所以说,如果有人已经根据上星期的内容在资料库里新增了一个表格,那较简单的方法可能是把 Heroku Postgres 这个扩充元件给删了,再重新新增一个。当然,如果对 SQL 语法以及psycopg2
熟悉的朋友,也可以用删掉表格 (DROP TABLE
,详细内容可以参考第 15 天)、改动表格 (ALTER TABLE
) 等等方式来做修改。对于造成的不便,再次向大家道歉。
接着我们需要一个检查使用者资料是否存在的函式check_record
:
app/custom_models/CallDatabase.py
def check_record(user_id): conn, cursor = access_database() postgres_select_query = f"SELECT * FROM user_dualtone_settings WHERE user_id = '{user_id}';" cursor.execute(postgres_select_query) user_settings = cursor.fetchone() return user_settings
如果没有纪录,那就先初始化一笔暂时的纪录:
app/custom_models/CallDatabase.py
def init_record(user_id, message_id): conn, cursor = access_database() table_columns = '(user_id, message_id, mode, gradient_factor, first_tone, second_tone)' postgres_insert_query = f"INSERT INTO user_dualtone_settings {table_columns} VALUES (%s,%s,%s,%s,%s,%s)" record = (user_id, message_id, 'blend', '50', 'red', 'blue') cursor.execute(postgres_insert_query, record) conn.commit() cursor.close() conn.close() return record
以及更新纪录的方法:
app/custom_models/CallDatabase.py
def update_record(user_id, col, value): conn, cursor = access_database() postgres_update_query = f"UPDATE user_dualtone_settings SET {col} = %s WHERE user_id = %s" cursor.execute(postgres_update_query, (value, user_id)) conn.commit() postgres_select_query = f"SELECT * FROM user_dualtone_settings WHERE user_id = '{user_id}';" cursor.execute(postgres_select_query) user_settings = cursor.fetchone() cursor.close() conn.close() return user_settings
全部写好之后,我们的档案架构看起来会像这样:
D:\appendix>tree /fFolder PATH listingVolume serial number is 9C33-6XXDD:.│ runtime.txt│ requirements.txt│ Procfile│ Alma.py│└───app │ __init__.py │ models_for_line.py │ routes.py │ └───custom_models AlmaTalks.py CallDatabase.py
好了,那么把完成的资料夹丢到 Heroku 上面吧!
棒!是不是真的记住了我们的设定呢。
图十、记住所有设定的 LINE BOT
等等,说好今晚的双色打光呢?
抱歉,今晚已经有点晚了。这个我们留到下星期,讨论如何利用 Heroku 暂存空间的同时,再一起把所有东西补上。有兴趣的读者,也可以试着利用上面我们讨论出来的双色打光程式码,装备到 LINE BOT 上,看看 Heroku 是否跑得动这个双色打光的杰出操作 (当然是跑得动,不然这个系列文就?)。
好的,相信大家知道接下来又要进入最重要的工商时间了。是的,感谢 iT邦帮忙 和 博硕文化,LINE Bot by Python 全攻略 集结成书了,欢迎有兴趣的大家前往预购喔。