FEATURED · 精选文章

Lua核心概念与实战应用:从表、函数到游戏开发与Redis脚本

发布时间 / 2026/8/26 9:21:10
来源 / 创域科博编辑部
栏目 / 资讯中心
Lua核心概念与实战应用:从表、函数到游戏开发与Redis脚本 1. 项目概述为什么我们需要“通俗易懂”的Lua如果你接触过游戏开发、嵌入式系统或者用过像Redis、Nginx这样的软件那你大概率已经和Lua打过照面了。它常常被描述为“小巧”、“高效”、“嵌入容易”但对于很多刚入门的朋友来说这些官方描述背后是面对一堆简洁到近乎“高冷”的语法和概念时的茫然。我见过不少开发者被“小巧”吸引而来却卡在了“为什么这个表要这么用”、“函数和闭包到底啥关系”这类问题上。所以今天我们不谈那些宏大的架构就坐下来像朋友聊天一样把Lua里那些最核心、最常用但也最容易让人迷糊的点掰开揉碎了讲清楚。我们的目标很简单让你看完之后能真正看懂一段Lua脚本并且有自信动手写点实用的东西无论是给游戏写个简单的行为逻辑还是为你的Web服务写个Nginx插件。Lua的魅力在于它的“双重人格”。一方面它作为一门独立的脚本语言语法简洁学习曲线相对平缓另一方面它作为一门优秀的嵌入式语言能像乐高积木一样无缝嵌入到C/C等宿主程序中为其提供灵活的扩展能力。这种特性让它成为了众多领域里的“幕后英雄”。但正因为它常扮演“配角”很多教程要么过于深入其C API的实现细节让初学者望而生畏要么过于浅显只讲“Hello World”遇到实际问题依然束手无策。我希望在这篇分享里能找到一个平衡点既讲透核心概念又紧扣实际应用场景比如如何用Lua脚本优化Redis操作或者如何理解游戏里那些控制角色行为的脚本。2. 核心概念拆解从“表”开始理解Lua的世界要搞懂Lua你必须先过“表”这一关。在Lua里表是唯一的数据结构。这句话不是夸张数组、字典、对象、模块、包——所有这些你在其他语言里熟悉的概念在Lua里统统用“表”来实现。理解了表你就理解了Lua的一半。2.1 表不只是数组或字典很多教程会告诉你Lua的表既可以当数组用也可以当字典用。这没错但理解得太浅。我更愿意说Lua的表是一个关联数组其索引可以是除了nil之外的任何值。这是它强大和灵活的根源。-- 作为一个数组索引为连续整数 local arr {“apple”, “banana”, “cherry”} print(arr[1]) -- 输出apple注意Lua索引默认从1开始 -- 作为一个字典或称哈希表、映射 local dict {name “Bob”, age 30, [“city”] “Shanghai”} print(dict.name) -- 输出Bob (等价于 dict[“name”]) print(dict[“city”]) -- 输出Shanghai -- 索引可以是任何类型比如函数虽然不常用 local key function() return “dynamic_key” end local weirdTable {} weirdTable[key] “Value for a function key”这里有一个至关重要的细节dict.name这种写法是dict[“name”]的语法糖。这意味着当你使用点号访问时Lua实际上是在用字符串作为键去查找。而dict[name]这里name是一个变量则是用变量name的值作为键。这两者天差地别混淆是新手最常见的错误之一。注意Lua表的数组部分连续整数索引和哈希部分其他索引在内部是分开存储和优化的。当你像数组一样使用表时尽量保持索引的连续性。如果中间有nil值如t[1]1; t[3]3那么这个表在大部分情况下会被完全当作哈希表来处理遍历性能会下降。使用ipairs遍历数组部分遇到nil会停止使用pairs遍历所有元素。2.2 函数与闭包Lua的“一等公民”在Lua中函数是“一等公民”。这意味着函数可以像数字、字符串一样被赋值给变量作为参数传递或者作为其他函数的返回值。这是实现高阶函数和闭包的基础。-- 函数定义与赋值 local function add(a, b) return a b end -- 等价于 local add function(a, b) return a b end -- 函数作为参数高阶函数 local function applyTwice(func, value) return func(func(value)) end local double function(x) return x * 2 end print(applyTwice(double, 5)) -- 输出20 (5*210, 10*220)闭包是理解Lua函数的关键进阶点。当一个函数内部引用了其外部作用域的局部变量时就形成了一个闭包。这个内部函数“记住”了它被创建时的环境。local function createCounter() local count 0 -- 外部局部变量 return function() -- 返回一个匿名函数这是一个闭包 count count 1 return count end end local myCounter createCounter() print(myCounter()) -- 输出1 print(myCounter()) -- 输出2 print(myCounter()) -- 输出3 local anotherCounter createCounter() print(anotherCounter()) -- 输出1 (独立的闭包拥有独立的count环境)在这个例子里内部的匿名函数和它“记住”的变量count组合起来就是一个闭包。每次调用createCounter()都会创建一个新的count变量和一个新的闭包。myCounter和anotherCounter互不影响。闭包是Lua实现面向对象、迭代器、状态保存等高级特性的基石比如我们常用的迭代器ipairs、pairs其内部就是通过闭包来实现状态追踪的。2.3 模块与包如何组织你的代码当脚本变多我们就需要模块化。Lua的模块系统非常简单直接一个模块就是一个返回一个表的Lua文件。这个表包含了模块所有要对外暴露的函数、变量等。假设我们有一个名为mymodule.lua的文件-- mymodule.lua local M {} -- 创建一个局部表代表模块 local privateVariable “I‘m private” -- 局部变量外部无法访问 function M.publicFunction() print(“This is a public function.”) print(“I can access private variable:”, privateVariable) end function M.add(a, b) return a b end return M -- 关键返回这个模块表在另一个Lua文件中我们可以这样使用它-- main.lua local mymodule require(“mymodule”) -- require会加载并执行mymodule.lua并接收其返回值 mymodule.publicFunction() -- 输出This is a public function... I can access... local sum mymodule.add(10, 20) print(sum) -- 输出30require函数是加载模块的核心。它会检查模块是否已加载缓存在package.loaded表中避免重复加载。根据模块名如“mymodule”在定义好的路径package.path中查找对应的.lua文件。加载并执行该文件。将文件的返回值通常是模块表存储到package.loaded中并返回这个值。实操心得require使用的路径分隔符是?。例如package.path可能是“./?.lua;/usr/local/share/lua/5.4/?.lua”。当执行require(“foo.bar”)时Lua会尝试查找./foo/bar.lua和/usr/local/share/lua/5.4/foo/bar.lua。理解这个机制对于部署和解决“module not found”错误至关重要。另外模块文件最后的return M语句必不可少否则require得到的将是true表示文件已执行而非模块接口。3. 核心语法精讲与避坑指南掌握了表和函数Lua的语法对你来说就透明了一大半。但魔鬼藏在细节里一些语法特性如果理解不透写出来的代码就容易出bug。3.1 控制结构if、for、while的细节Lua的控制结构很直观但有几个点需要特别注意。if语句Lua中只有false和nil被视为“假”其他所有值包括数字0、空字符串“”、空表{}都被视为“真”。这个逻辑和很多语言如C、Python不同务必牢记。local x 0 local t {} if x then print(“x is true!”) -- 这行会被执行因为0为真 end if t then print(“t is true!”) -- 这行也会被执行空表也为真 end if not nil then print(“nil is false!”) -- 这行会被执行 endfor循环Lua有两种for循环。数值for和泛型for。数值forfor var start, end, step do ... end。注意循环变量var是局部于循环体的并且在循环结束后你就不能再访问它。step可以是负数用于递减循环。for i 1, 5 do print(i) -- 输出1,2,3,4,5 end -- print(i) -- 这里会报错i是nil因为它的作用域只在循环内 for j 10, 1, -2 do print(j) -- 输出10,8,6,4,2 end泛型for通常与迭代器函数配合使用如pairs,ipairs。它的形式是for k, v in pairs(t) do ... end。这里的k和v同样也是循环的局部变量。while和repeat循环while是先判断后执行repeat...until是先执行后判断。until后面的条件为真时停止循环注意这里是“为真时停止”和while的“为真时继续”逻辑相反。local i 1 while i 3 do print(“while:”, i) i i 1 end local j 1 repeat print(“repeat:”, j) j j 1 until j 3 -- 当j3为真时停止循环3.2 字符串与常用库函数Lua的字符串是不可变的。任何看似修改字符串的操作如string.sub,string.gsub实际上都是创建了一个新的字符串。对于频繁的字符串拼接操作这样做效率很低。这时就需要用到table.concat。-- 低效的拼接尤其在大循环中 local result “” for i 1, 10000 do result result .. “line” .. i .. “\n” -- 每次拼接都产生新字符串内存和CPU开销大 end -- 高效的拼接 local t {} for i 1, 10000 do t[#t 1] “line” .. i -- 将字符串放入表中 end local result table.concat(t, “\n”) -- 一次性连接高效string.gsub是一个非常强大的全局替换函数它支持模式匹配正则表达式的一个子集。local s “Hello, my name is John Doe. John is a programmer.” -- 将所有的“John”替换为“Alice” local new_s, count string.gsub(s, “John”, “Alice”) print(new_s) -- 输出Hello, my name is Alice Doe. Alice is a programmer. print(“Replaced”, count, “times”) -- 输出Replaced 2 timestable库是操作表的利器。table.insert(t, pos, value)用于插入元素table.remove(t, pos)用于删除元素。table.sort(t)可以对表进行原地排序。对于数组部分的遍历和操作这些函数非常方便。local fruits {“banana”, “apple”, “orange”} table.insert(fruits, 2, “grape”) -- 在位置2插入 -- fruits 变为 {“banana”, “grape”, “apple”, “orange”} table.remove(fruits, 3) -- 删除位置3的元素“apple” -- fruits 变为 {“banana”, “grape”, “orange”} table.sort(fruits) -- 按字母顺序排序 -- fruits 变为 {“banana”, “grape”, “orange”}3.3 元表与元方法让表拥有“超能力”元表是Lua中最强大也最复杂的特性之一。你可以把它理解为表的“操作说明书”。通过设置元表我们可以自定义表在特定操作如相加、索引、调用时的行为。设置与获取元表local myTable {} local metaTable {} -- 这个表将作为myTable的元表 setmetatable(myTable, metaTable) -- 设置元表 local mt getmetatable(myTable) -- 获取元表mt就是metaTable__index元方法这是使用最频繁的元方法。当访问一个表中不存在的键时Lua会触发__index元方法。它可以是一个函数也可以是另一个表。-- __index 为表的情况实现继承 local parent {money 100} local child {} setmetatable(child, {__index parent}) -- 设置父表为元表的__index print(child.money) -- 输出100。child本身没有money但通过__index找到了parent中的money -- __index 为函数的情况实现计算属性或默认值 local smartTable {} setmetatable(smartTable, { __index function(table, key) if key “dynamicValue” then return os.time() -- 返回当前时间戳作为动态值 else return “default_value_for_” .. key end end }) print(smartTable.dynamicValue) -- 输出一个时间戳数字 print(smartTable.someKey) -- 输出default_value_for_someKey__newindex元方法当给一个表中不存在的键赋值时会触发__newindex。它可以用来控制赋值行为比如实现只读表、或者将数据存储到另一个地方。local readOnlyData {config “default”} local proxy {} setmetatable(proxy, { __index readOnlyData, -- 读操作转发到readOnlyData __newindex function(table, key, value) -- 写操作被禁止 error(“Attempt to write to a read-only table: ” .. key) end }) print(proxy.config) -- 输出default -- proxy.newKey “value” -- 这行会抛出错误Attempt to write to a read-only table: newKey算术运算相关的元方法如__add加、__mul乘等允许我们让表参与运算。local Vector2D {} Vector2D.__index Vector2D function Vector2D.new(x, y) local v {x x or 0, y y or 0} setmetatable(v, Vector2D) return v end -- 定义向量相加的行为 function Vector2D.__add(a, b) return Vector2D.new(a.x b.x, a.y b.y) end local v1 Vector2D.new(1, 2) local v2 Vector2D.new(3, 4) local v3 v1 v2 -- 触发 __add 元方法 print(v3.x, v3.y) -- 输出4 6避坑指南元表虽然强大但过度使用会让代码逻辑变得隐晦难以调试。一个常见的坑是__index和__newindex的递归调用。如果在__index函数中又触发了对原表的索引访问可能会导致无限递归。务必确保你的元方法逻辑清晰且有终止条件。对于简单的数据共享优先考虑组合而非复杂的元表继承。4. 实战应用Lua在真实场景中的身影理解了核心概念我们来看看Lua在实际中是如何大显身手的。这能帮你把抽象的知识和具体的问题联系起来。4.1 游戏开发角色行为与配置在游戏开发中Lua常被用于编写非核心的游戏逻辑如NPC的对话、任务系统、技能效果、UI界面逻辑等。这样做的好处是修改这些逻辑无需重新编译整个C工程热更新也变得非常方便。假设我们有一个简单的NPC对话配置-- npc_dialogue.lua local dialogues { [1] { text “你好冒险者”, options { {text “你是谁”, next 2}, {text “再见。”, next -1} -- -1 表示结束对话 } }, [2] { text “我是这里的铁匠。”, options { {text “有什么可以帮你的”, next 3}, {text “我得走了。”, next -1} } } } -- 一个简单的对话处理函数 function getDialogue(id) return dialogues[id] end -- 游戏的主C引擎会加载这个脚本并调用getDialogue函数来获取对话内容 -- 根据玩家的选择next值跳转到下一句。游戏引擎C通过Lua的C API调用这些函数获取配置数据驱动游戏运行。当策划想调整对话内容时只需要修改这个.lua文件游戏运行时重新加载即可生效实现了策划与程序的解耦。4.2 Redis与Lua脚本实现原子性操作Redis支持使用Lua脚本这带来了一个巨大的优势原子性。在脚本执行期间Redis不会执行其他命令因此你可以用Lua脚本实现复杂的多步操作而不用担心竞态条件。一个经典场景是“限流”或“库存扣减”。假设我们要实现一个简单的访问频率限制一个用户key在60秒内最多访问10次。-- rate_limiter.lua local key KEYS[1] -- 用户标识由Redis客户端传入 local limit tonumber(ARGV[1]) -- 限制次数例如10 local window tonumber(ARGV[2]) -- 时间窗口例如60秒 local current redis.call(“GET”, key) local count 0 if current then count tonumber(current) end if count limit then -- 超过限制返回0表示拒绝 return 0 else -- 未超过限制计数加1 redis.call(“INCR”, key) if count 0 then -- 如果是第一次设置需要设置过期时间 redis.call(“EXPIRE”, key, window) end return 1 -- 返回1表示允许 end在Redis客户端如命令行或编程语言SDK中你可以这样调用EVAL “上面那段Lua脚本” 1 user:123 10 601表示后面有1个KEYuser:123。user:123是传入的KEYS[1]。10和60是传入的ARGV[1]和ARGV[2]。这个脚本保证了“判断-计数-设置过期时间”这一系列操作是原子的不会因为并发请求而出错。这就是Lua脚本在Redis中的核心价值。4.3 Nginx与OpenResty高性能Web逻辑OpenResty将Nginx与LuaJIT一个高性能的Lua即时编译器深度集成允许你直接在Nginx的各个处理阶段如访问、内容生成、日志记录注入Lua代码。这让你能用Lua写出高性能的网关、API聚合器、防火墙逻辑等。一个简单的例子在Nginx中直接用Lua生成响应内容# nginx.conf 中的某个 location 配置 location /hello-lua { default_type ‘text/plain‘; content_by_lua_block { local name ngx.var.arg_name or “Anonymous“ -- 获取URL参数 ngx.say(“Hello, ”, name, “!“) ngx.say(“Time is: ”, os.date(“%Y-%m-%d %H:%M:%S“)) -- 可以在这里进行复杂的逻辑比如访问数据库、调用外部API等 } }访问/hello-lua?nameBobNginx会直接返回由Lua代码生成的“Hello, Bob!”和当前时间完全绕过了静态文件或后端应用服务器延迟极低。5. 开发环境、调试与高效工具链工欲善其事必先利其器。一个好的开发环境能极大提升你学习和使用Lua的效率。5.1 编辑器与IDE选择对于Lua开发你不需要特别重型IDE。一个轻量级但功能强大的代码编辑器足矣。VSCode目前最主流的选择。安装“Lua”扩展由 sumneko 开发它能提供近乎完美的语法高亮、智能补全、定义跳转、代码诊断等功能。它甚至能理解你项目的文件结构对require语句的路径进行准确的补全和跳转。IntelliJ IDEA / CLion如果你主要进行C/C与Lua混合的嵌入式开发如游戏客户端JetBrains系列的IDE配合“EmmyLua”插件是非常专业的选择。它能更好地处理C API和Lua之间的交互提示。Sublime Text / Atom轻量级编辑器的经典之选配合相应的Lua语法包体验也很流畅。注意网上有朋友提到“editplus 没有lua模板”。EditPlus是一款老牌的文本编辑器其对Lua的支持确实较弱。如果你坚持使用需要手动配置语法高亮文件.stx文件。但出于效率考虑强烈建议切换到VSCode或上述其他现代编辑器。5.2 调试从简单到专业调试是开发中不可或缺的一环。Lua的调试方式多样。1. 最朴素的调试print大法不要小看print。在关键位置打印变量值、函数入参、执行流程标记是最快、最直接的调试手段。配合Lua的...可变参数和table.concat可以输出结构化的信息。local function debugPrint(tag, ...) local args {...} local msg table.concat(args, “\t”) print(string.format(“[%s] %s”, tag, msg)) end debugPrint(“STEP1”, “value of x:”, x, “value of t:”, table.concat(t, “,“))2. 使用内置的debug库Lua提供了debug标准库功能强大但需谨慎使用不当使用会影响性能或破坏程序状态。debug.traceback()获取当前的调用栈信息在错误处理时非常有用。local status, err pcall(someRiskyFunction) if not status then print(“Error:”, err) print(debug.traceback()) enddebug.getinfo()获取函数的信息。debug.getlocal(),debug.setlocal()获取和设置局部变量。通常用于构建调试器3. 使用专业的调试器对于复杂项目一个图形化调试器是必需的。VSCode Lua Debugger 扩展这是目前最推荐的调试方案。配置好启动配置launch.json可以设置断点、单步执行、查看调用栈、监控变量包括表结构的展开体验和调试其他语言无异。ZeroBrane Studio一个专门为Lua设计的轻量级IDE内置了优秀的调试器支持远程调试对游戏开发如Love2D, Corona SDK和嵌入式脚本调试非常友好。关于“Redis调试Lua脚本用什么开发工具调试IDEA能调试么”直接调试运行在Redis服务器内的Lua脚本比较困难。常见的做法是本地单元测试将脚本的逻辑提取出来在本地用Lua环境进行逻辑测试。Redis的SCRIPT DEBUG命令Redis提供了SCRIPT DEBUG YES/SYNC/NO命令。设置SYNC模式后Redis会阻塞直到有调试器连接。然后你可以使用redis-cli --ldbLua Debugger进入一个简单的命令行调试环境支持单步执行、打印变量等。但这并非图形化调试。IDEA/CLion EmmyLua 远程调试对于复杂的、与C交互的Lua代码比如游戏可以通过EmmyLua插件和调试库如emmy_core实现远程调试但这通常需要项目本身集成调试库支持。纯Redis环境不适用此方法。5.3 包管理LuaRocks当你的项目需要依赖第三方库时就需要包管理器。LuaRocks是Lua事实上的标准包管理器。安装通常可以从官网下载安装或者通过系统的包管理器如macOS的brewLinux的apt/yum安装。基本使用# 搜索包 luarocks search luasocket # 安装包 (默认安装到系统目录可能需要sudo) luarocks install luasocket # 安装指定版本 luarocks install luasocket 3.0rc1-2 # 为当前项目安装包推荐避免污染全局环境 # 首先在项目目录初始化 luarocks init # 然后使用 --tree 参数安装到本地 luarocks install --tree ./lua_modules luasocket安装后在你的Lua脚本中就可以通过require(“socket”)来引入luasocket库了。如果安装在本地./lua_modules你需要在运行脚本前将./lua_modules/share/lua/5.4/或对应版本路径加入到Lua的package.path中或者使用luarocks自带的luarocks path命令来设置环境变量。6. 常见问题与排查技巧实录在实际编写和运行Lua代码时你一定会遇到各种各样的问题。这里记录了一些典型问题和我的解决思路。6.1 语法与运行时错误错误attempt to call a nil value (global ‘xxx’)原因你尝试调用一个值为nil的变量就像把它当函数一样用了。排查检查函数名是否拼写错误。Lua是大小写敏感的。检查函数是否正确定义或者是否在调用之前被定义Lua是顺序执行的。检查你是否require了包含该函数的模块并且模块导出正确。示例local mymodule require(“mymodule”) mymodule.publicFuntion() -- 拼写错误应该是 publicFunction错误attempt to index a nil value (field ‘xxx’)原因你尝试对一个nil值进行索引操作比如t.key或t[“key”]其中t是nil。排查检查变量t是否被正确初始化。是不是忘了t {}检查从函数返回的表是否为nil。在索引前进行判空保护if t and t.key then ... end错误unexpected symbol near ‘xxx’原因语法错误。可能是缺少end、then或者字符串引号没有闭合中文标点等。排查仔细检查错误行附近代码的语法结构。使用编辑器的语法高亮和括号匹配功能能帮助快速定位。6.2 逻辑错误与调试技巧问题表的行为不符合预期尤其是当表同时用作数组和字典时。场景你用一个表存储了一些数据但#操作符获取的长度不对或者ipairs遍历时漏掉了一些元素。根因#和ipairs只遍历表的“数组部分”即键为连续正整数的部分。如果你在表中混用了非数字键或非连续的数字键这部分数据不会被它们处理。解决如果需要一个纯数组就只用连续整数作为键从1开始。如果需要存储键值对就明确使用字符串等作为键并用pairs遍历。如果需要同时处理心里要清楚#和ipairs的局限性。获取“所有元素数量”可以用一个循环配合pairs来计数。local function tableSize(t) local count 0 for _ in pairs(t) do count count 1 end return count end问题函数修改了传入的参数表但这不是我想要的。场景Lua中表、函数、线程等是“引用类型”。当你将一个表作为参数传递给函数时传递的是引用类似指针函数内部对表的修改会影响到外部的原始表。解决如果不想修改原表需要在函数内部创建表的副本深拷贝或浅拷贝取决于需求。local function shallowCopy(original) local copy {} for k, v in pairs(original) do copy[k] v end return copy end local t1 {a 1, b {c 2}} local t2 shallowCopy(t1) t2.a 99 -- 不影响 t1.a t2.b.c 88 -- 这会影响到 t1.b.c因为只拷贝了第一层引用。 -- 如果需要完全独立的副本需要实现深拷贝函数。问题require模块时提示module ‘xxx’ not found排查步骤检查路径打印package.path和package.cpath看你的模块文件是否在这些路径下。require(“foo.bar”)会查找foo/bar.lua。检查文件扩展名require通常查找.lua文件。确保你的文件是.lua后缀。检查文件内容模块文件最后必须return一个值通常是模块表。如果没有returnrequire会返回true但你将其当作表来访问就会出错。避免循环依赖模块Arequire模块B模块B又require模块A这会导致问题。需要重构代码解耦。6.3 性能相关注意事项字符串拼接如前所述在循环中避免使用..拼接大量字符串改用table.concat。表的大小预分配如果你事先知道一个数组表的大致大小可以在创建时预分配避免多次重新哈希。-- 预分配一个大小为1000的数组 local bigArray {} for i 1, 1000 do bigArray[i] 0 -- 先填充占位 end -- 然后再进行赋值操作效率会更高局部变量总是使用local关键字声明局部变量。访问局部变量比访问全局变量快得多。在循环外将频繁访问的全局函数或模块函数赋值给局部变量也能提升性能。local sin math.sin -- 将全局函数缓存为局部变量 for i 1, 1000000 do local x sin(i) -- 现在调用的是局部变量sin end理解JIT如果使用LuaJITLuaJIT的即时编译器能极大提升性能但它对代码模式有要求。比如避免在经常执行的循环热代码中使用无法被JIT编译的特性如某些debug函数、非标准化的FFI调用模式等。保持循环结构简单、类型稳定有助于JIT发挥最大效力。Lua的精髓在于其简洁与灵活之间的平衡。它没有试图包含一切而是通过强大的元机制如表、元表赋予了你构建一切的能力。开始时可能会觉得有些概念绕但一旦你习惯了用“表”来思考用“函数”来构建逻辑你就会发现它的优雅和高效。多读优秀的开源代码比如OpenResty中的一些Lua库多动手写从小的工具脚本开始逐渐尝试更复杂的模块和项目是掌握这门语言的最佳路径。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻