[插件] 网盘互助(netdisk_help)简易版插件发布

👑Lv.11 元老 🌏 正式会员
2026-09-29 16:55:14

功能:私密悬赏求助 —— 用户发「网盘资源求助」并冻结悬赏积分,他人「应助」提交网盘链接, 求助者采纳后积分发放给应助者。应助内容仅「求助者 + 应助者 + 管理员」三方可见。


一、功能简介

  • 发布求助:标题 + 富文本正文 + 悬赏积分;发布时冻结(扣除)悬赏积分
  • 两种发布入口(见 §二)
    • ① 发帖页内嵌:发帖时顺带挂一条求助,帖子标题/正文即求助内容
    • ② 独立页:/netdisk-help/post/new,自己填标题/正文
  • 应助:他人提交网盘链接(+ 可选提取码 + 备注);同一求助同一人只能应助一次
  • 采纳:仅求助者本人可采纳;采纳后冻结的积分发放给应助者,求助转终态
  • 取消 / 过期:求助者主动取消、或有效期到期 → 积分原额退回,求助转终态
  • 隐私保护:应助内容在视图层按三方可见性裁剪,非相关人看不到任何应助信息
  • 前台页面:求助列表(状态筛选)/ 我的互助(双 Tab)/ 求助详情 / 发布表单
  • 后台管理:总览 + 设置 / 求助列表(跨库关键词搜索 + 状态筛选)/ 详情 / 强制终止退分 / 删除
  • 全局入口:PC 左栏「应用」面板 + 移动端「应用」下拉(nav_plugin_links 钩子)
  • 前端交互:危险操作二次确认(Alpine.js),零内联 JS

二、两个发布入口

入口 触发位置 标题 / 正文来源 thread_id 说明
发帖页内嵌 post/create 页提交按钮之前(折叠区) 帖子自己的标题/正文 该帖 id 只填悬赏积分;thread_create_after 在发帖事务 commit 后创建
独立页 /netdisk-help/post/new 自己填 0 与任何帖子无关

两条路径最终都汇到同一个 Plugin::createRequest() —— 积分冻结、配额校验、事务与补偿逻辑完全共用,不存在两套实现。

关联机制:

  • ndh_requests.thread_id 记录「这条求助属于哪一帖」(0 = 独立页发布)
  • 帖子详情页内嵌区(thread_redpacket_area 钩子)按 thread_id 查「这一帖自己发过求助吗」, 而不是「该帖所属版块是否启用」—— 后者会让版块内每个主题都挂上别人的求助入口,不是产品要的行为。
  • 该帖没发过求助 → 内嵌区零输出(一个字节都不打印); 但只要 session 里有待展示的发帖附带提示,内嵌区仍要输出(见 §六 注意事项)。

三、状态流转

                   ┌─────────────┐
                   │   pending   │  求助中(积分已冻结)
                   └──────┬──────┘
                          │ 他人应助
                          ▼
                   ┌─────────────┐
                   │  assisting  │  已有人应助(等待采纳)
                   └──────┬──────┘
                          │ 求助者采纳
                          ▼
                   ┌─────────────┐
                   │  completed  │  已完成(积分已发给应助者)
                   └─────────────┘

  pending / assisting ──求助者取消──▶ cancelled(积分原额退回)
  pending / assisting ──有效期到───▶ expired  (积分原额退回)
状态 常量 含义 积分归属
pending STATUS_PENDING 求助中 已冻结
assisting STATUS_ASSISTING 已有人应助,待采纳 已冻结
completed STATUS_COMPLETED 已完成 已发放给应助者
expired STATUS_EXPIRED 已过期 已退回求助者
cancelled STATUS_CANCELLED 已取消 已退回求助者
  • 终态集合 FINAL_STATUSES = [completed, expired, cancelled]:进入后不可再流转,积分已结清
  • 应助记录状态:pending(待采纳)/ accepted(已采纳)

四、目录结构

plugins/netdisk_help/
├── plugin.json                      # 插件清单(8 个钩子 + admin_url + permissions)
├── Plugin.php                       # 主类:生命周期、建表补列、配置读取、业务数据访问(控制器与 CLI 共用)
├── FrontController.php              # 前台控制器(列表/我的/发布/详情/应助/采纳/取消/cron/我的互助)
├── AdminController.php              # 后台控制器(总览/设置/求助管理/详情/终止/删除)
├── hook/
│   ├── init_after.php               # 惰性建表兜底(门控标记含 SCHEMA_VERSION)
│   ├── route_register.php           # 前台路由注册(9 条)
│   ├── admin_route_register.php     # 后台路由注册(7 条)
│   ├── layout_head_end.php          # 前台样式注入(仅插件页 / thread/show / post/create)
│   ├── nav_plugin_links.php         # PC 左栏「应用」+ 移动端「应用」下拉入口
│   ├── thread_redpacket_area.php    # 帖子详情页内嵌区(按 thread_id 判定 + PRG 一次性提示)
│   ├── thread_create_after.php      # 发帖附带资源求助(事务 commit 后创建)
│   └── post_create_extra.php        # 发帖页「附带资源求助」折叠表单
├── views/
│   ├── front_list.php               # 求助列表(状态筛选 + 分页)
│   ├── front_mine.php               # 我的互助(我发布的 / 我应助的,双 Tab)
│   ├── front_new.php                # 发布求助表单(独立页,带核心富文本编辑器)
│   ├── front_detail.php             # 求助详情(应助区按三方可见性裁剪)
│   ├── admin.php                    # 后台总览 + 设置
│   ├── admin_requests.php           # 后台求助列表(搜索 + 筛选)
│   └── admin_request_detail.php     # 后台求助详情
├── assets/
│   └── style.css                    # 全部样式(`.ndh-*` 前缀,零内联)
├── data/
│   ├── netdisk_help.sqlite          # 插件独立库
│   ├── .htaccess / web.config       # 拒绝直接访问数据目录
│   └── runtime/                     # ⚠️ 运行期目录(过期节流标记),自动创建,**打包须排除**
└── lang/
    ├── zh.php / en.php / zh_tw.php  # 三语字典(各 179 键,键集合零差异)

五、数据表

插件独立库:plugins/netdisk_help/data/netdisk_help.sqlite

ndh_requests(求助主表)

列 类型 说明
id INTEGER PK
user_id INTEGER NOT NULL 求助者
category_id INTEGER NOT NULL DEFAULT 0 归属版块(0 = 未指定)
thread_id INTEGER NOT NULL DEFAULT 0 关联帖子(0 = 独立页发布)
title TEXT NOT NULL 标题(上限 100 字)
content TEXT NOT NULL DEFAULT '' 正文(富文本 HTML 或纯文本,上限 2000 可见字符)
points INTEGER NOT NULL DEFAULT 0 悬赏积分(发布时冻结)
status TEXT NOT NULL DEFAULT 'pending' 状态(见 §三)
helper_id INTEGER NOT NULL DEFAULT 0 采纳后的应助者
accepted_at INTEGER NOT NULL DEFAULT 0 采纳时间(unix 秒)
expires_at INTEGER NOT NULL DEFAULT 0 到期时间(unix 秒;0 = 永不过期)
created_at INTEGER NOT NULL 创建时间(unix 秒)
updated_at INTEGER NOT NULL DEFAULT 0 更新时间(unix 秒)

ndh_answers(应助表)

列 类型 说明
id INTEGER PK
request_id INTEGER NOT NULL 所属求助
user_id INTEGER NOT NULL 应助者
link TEXT NOT NULL DEFAULT '' 网盘链接(上限 500 字)
code TEXT NOT NULL DEFAULT '' 提取码(上限 50 字)
note TEXT NOT NULL DEFAULT '' 备注(上限 500 字,纯文本)
status TEXT NOT NULL DEFAULT 'pending' pending / accepted
created_at INTEGER NOT NULL 创建时间(unix 秒)
accepted_at INTEGER NOT NULL DEFAULT 0 采纳时间(unix 秒)

索引(8 个)

索引 表 列
idx_ndh_requests_user ndh_requests (user_id, status, id DESC)
idx_ndh_requests_status ndh_requests (status, id DESC)
idx_ndh_requests_expires ndh_requests (status, expires_at)
idx_ndh_requests_cat ndh_requests (category_id, id DESC)
idx_ndh_requests_thread ndh_requests (thread_id)
idx_ndh_answers_unique ndh_answers (request_id, user_id) UNIQUE
idx_ndh_answers_request ndh_answers (request_id, status, id)
idx_ndh_answers_user ndh_answers (user_id, id DESC)

⚠️ idx_ndh_requests_cat / idx_ndh_requests_thread 依赖「后加的列」,不能写进 ddlStatements() (存量库上 CREATE TABLE IF NOT EXISTS 无效 → 建索引会抛 no such column → 中断整个 activate()), 只能登记在 ensureColumnPatches() 里、且写在补列之后。

六、路由

前台(9 条)

方法 路径 说明
GET /netdisk-help 求助列表(?status= 状态筛选、?page=N)
GET /netdisk-help/mine 我的互助:我发布的 / 我应助的(需登录)
GET /netdisk-help/post/new 发布求助表单(需登录;⚠️ 必须排在 /post/{id} 之前)
POST /netdisk-help/post 提交求助(需登录 + CSRF + 限流)
GET /netdisk-help/post/{id} 求助详情(应助区按三方可见性裁剪)
POST /netdisk-help/post/{id}/answer 提交应助(需登录 + CSRF + 限流)
POST /netdisk-help/post/{id}/accept 采纳应助(需登录 + CSRF + 限流,仅求助者本人)
POST /netdisk-help/post/{id}/cancel 取消求助(需登录 + CSRF + 限流,仅求助者本人)
GET /netdisk-help/cron/expire 到期扫描入口(外部计划任务调用,见 §八)

⚠️ 注册顺序 = 匹配顺序:{id} 是 [^/]+,会吃掉字面量 new → 具体路由必须写在参数路由之前(本插件已如此排列)。

后台(7 条,核心自动补 /admin 前缀)

方法 路径 说明
GET /admin/netdisk-help 总览(统计 + 版块启用状态)
GET /admin/netdisk-help/settings 设置页
POST /admin/netdisk-help/settings 保存设置(CSRF)
GET /admin/netdisk-help/requests 求助列表(关键词搜索 + 状态 / 分类筛选)
GET /admin/netdisk-help/requests/{id} 求助详情(含全部应助)
POST /admin/netdisk-help/requests/{id}/close 强制终止(转 cancelled / expired 并退分,CSRF)
POST /admin/netdisk-help/requests/{id}/delete 删除求助(仅终态,级联删应助,CSRF)

⚠️ 后台路由需声明 route:admin;写全局设置需 system:settings。二者均已声明。 ⚠️ 后台与前台共用同一个 Router:{id} 占位符按名传参(call_user_func_array)。

七、后台设置项(5 个)

设置存核心 settings 表,键前缀 ndh_:

键 默认 含义
ndh_max_points_percent 50 悬赏上限 = 用户当前积分的百分比(1~100)
ndh_default_expire_days 7 默认有效期(天,1~365)
ndh_expire_enabled 1 是否启用有效期(1 启用 / 0 关闭 → 永不过期)
ndh_daily_limit 5 每日发布求助上限(0 = 不限)
ndh_forum_ids '' 启用本功能的论坛版块(逗号分隔 id);空串 = 全站启用

⚠️ 开关类配置(ndh_expire_enabled)的有效值只允许 '0' / '1', 绝不能存空串 —— 空串会被 conf() 当成「未配置」回落默认值。 ⚠️ 上线新设置项后必须清一次缓存(settings_cache.php 不会自动补新键)。

八、过期机制(两条路径)

路径 入口 节流 单批上限
前台惰性 任意前台请求(expireDueThrottled()) 60 秒(data/runtime/expire_last.ts 标记) 50 条
cron 专用 GET /netdisk-help/cron/expire(expireAllForCron()) 无节流 500 条

cron 入口鉴权:

  • CRON_KEY 未配置 → 503,拒绝执行
  • 密钥取自请求头 X-Cron-Key 或查询参数 ?key=,用 hash_equals() 比对
  • 密钥无效 → 403
  • 成功 → 输出 ndh_expire_ok expired=N

生产环境(WSToolbox + 云函数)由 cron_trigger.php 每 5 分钟触发一次。 有效期关闭时(ndh_expire_enabled=0)前台惰性路径零 IO 短路。

九、权限与隐私设计

三方可见性原则

角色 能看到的应助
管理员 全部应助可见
求助者本人 全部应助可见
应助者本人 仅自己那条可见
其他任何人(含未登录) 一条内容都看不到,只知道「有几条」(total / hidden 计数)

实现口径(单点裁剪):

  • 唯一入口 Plugin::visibleAnswers($request, $viewerId, $isAdmin) —— 前台渲染一律走它
  • Plugin::canSeeAllAnswers($request, $viewerId, $isAdmin) —— 布尔判定
  • ⚠️ $isAdmin 必须由调用方用 Auth::isAdmin() 传入(插件不自行判定管理员)
  • ⚠️ 底层 Plugin::listAnswers() 仅供内部与后台使用,前台禁止直接调用

管理员判定口径

  • 唯一口径 Auth::isAdmin()(role === 'admin' 且 group_id === 4,AND 非 OR)
  • 插件不自造管理员判定(自造 = 越权高危)
  • ⚠️ 插件继承的是 app\Core\Controller,不会触发 Admin\BaseController 构造函数门禁 → 后台控制器必须自补门禁(本插件 AdminController 已补)

十、积分安全设计

红线:积分必须走核心 Points,插件不直写 users.points;且必须「先查余额再扣」,禁止先扣后查。

跨库事实:积分在 business.sqlite、插件表在自己的库 —— 两个 WAL 文件,跨库没有原生事务 → 采用本仓既有范式(同 points_mall)的三段式。

发布(createRequest)

  1. 校验与配额:标题/正文长度(可见字符)、悬赏 ≤ maxBountyFor(用户积分)、每日上限
  2. Points::deduct() 扣分(核心库原子防透支)
  3. 插件库自事务写入;失败 → Points::award() 补偿退回

结算(acceptAnswer / cancelRequest / 过期)

  1. 先抢状态(UPDATE ... WHERE status IN (...) 条件更新,抢到才继续)
  2. 再发钱(Points::award())
  3. 失败 → 补偿回滚状态

护栏

  • 退分前必须过 ownerRefundable($ownerId) —— Points::award() 对不存在的 uid 会 UPDATE users 命中 0 行(不报错)但 points_log 照样插入 → 必须先确认用户存在
  • 删除仅限终态(adminDelete):未结清就删 = 积分凭空消失
  • 审计流水不可动:不删核心库 points_log / notifications
  • 记账口径:points_log.reason / related_type = netdisk_help(Plugin::POINTS_TYPE)

十一、技术实现要点

核心接口(只用这些,零插件依赖)

Points::deduct / Points::award / Notification::add / Csrf / RateLimiter / Settings / Auth::isAdmin

富文本编辑器接入三步(独立页发布表单)

  1. 控制器传 'needsEditor' => true —— app/Views/layouts/main.php 据此加载 editor.css / editor.js, 不传就根本不加载
  2. 视图 <textarea id="ndh-content" name="content" data-editor="ndh-content" class="mn-textarea">
  3. 服务端必须 \app\Helpers\Content::decode() —— ⚠️ 编辑器提交的是 base64 (它隐藏原 textarea、移除其 name/required,另建同名隐藏域写入 base64)。 无 JS 兜底时提交纯文本,decode() 对非 base64 原样返回

正文渲染口径(唯一正解)

  • 前台详情 / 后台详情:Content::formatPostContent($raw) 且必须裸 echo (返回的已是净化过的 HTML,再套 $this->e() 会把标签当文本显示)
  • formatPostContent() 自动分流:
    • 纯文本 → htmlspecialchars + URL 自动链接 + nl2br
    • 含标签 → Emoji::parse + sanitizeHtml
  • 存量纯文本数据无需迁移(旧数据走纯文本分支,与旧 nl2br($this->e()) 等价)
  • ⚠️ 纯文本字段(应助备注 note)保持 nl2br($this->e(...)),不要套 formatPostContent

正文长度口径

  • 按 可见字符 计:mb_strlen(strip_tags($content))(Plugin::contentVisibleLen(),与核心 Content::lengthError() 同口径)
  • 超长截断(不报错):Plugin::clampContent() —— HTML 走 Content::excerpt()(DOM 级,禁 mb_substr 硬切),纯文本走 mb_substr
  • ⚠️ Content::excerpt() 截断会追加省略号 … → 结果可见字 = limit +1

前端与样式

  • CSS 前缀 .ndh-*,只用既有 --mn-* 变量,零硬编码色
  • 提权选择器禁「核心类 + 插件类」混合形式(如 .admin-table.ndh-admin-table td)→ 写 .ndh-admin-table td.ndh-col-act
  • 危险按钮直接用核心 .admin-content .btn-danger(本仓 --mn-danger 9 套主题 0 套定义)
  • 二次确认用 Alpine.js(x-on:submit.prevent="if(!confirm(...)) return; $el.submit()"),零内联门禁正则不匹配 x-on:submit
  • ⚠️ 插件 CSS 在 main.php:40 注入,早于核心 editor.css → 同权重核心后胜 → 只写核心没定义的属性

限流(RateLimiter::hitConfig)

动作 键 阈值
发布求助 netdisk_help_post 10 次 / 60 秒
提交应助 netdisk_help_answer 10 次 / 60 秒
采纳 netdisk_help_accept 20 次 / 60 秒
取消 netdisk_help_cancel 20 次 / 60 秒

钩子清单(8 个,见 plugin.json)

钩子 文件 作用
init_after hook/init_after.php 惰性建表兜底(门控标记含 SCHEMA_VERSION,命中时每请求仅 1 次 is_file()、0 条 SQL)
route_register hook/route_register.php 前台路由注册
admin_route_register hook/admin_route_register.php 后台路由注册
layout_head_end hook/layout_head_end.php 前台样式注入(仅插件页 / thread/show / post/create)
nav_plugin_links hook/nav_plugin_links.php PC 左栏「应用」+ 移动端「应用」下拉
thread_redpacket_area hook/thread_redpacket_area.php 帖子详情页内嵌区
thread_create_after hook/thread_create_after.php 发帖附带资源求助
post_create_extra hook/post_create_extra.php 发帖页折叠表单

十二、安装与启用

  1. 把 netdisk_help 整个目录放到 plugins/ 下
  2. 后台「插件管理」中启用(plugin.json 的 activated 已为 true)
  3. 首次请求时 init_after 钩子惰性建表(幂等),并补种默认配置
  4. 后台「设置」页按需调整 5 个配置项;调整后清一次缓存

非破坏式建表:CREATE TABLE IF NOT EXISTS + addColumnIfMissing() + createIndexIfMissing(), 对存量库安全(重复执行是 no-op)。

卸载(uninstall()):按依赖顺序删两张表 + 清建表标记 + 清运行期目录。 ⚠️ 不会删核心库 points_log / notifications(审计流水保留)。

环境要求:PHP 8.0+(实测 8.2)、SQLite(PDO)、FlintHub 核心。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 82 | Replies 14

All Replies (14)

🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 18:08:10
“视图层裁剪”这点得掰扯清楚:三方可见性必须在服务端模板渲染时就判定,别指望 CSS/JS 藏 DOM,F12 一展开全露,白搭。零内联 JS 配 Alpine 记得给 CSP 挂 nonce
#1 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 18:11:58
状态流转用 FINAL_STATUSES 收口挺好,但 `thread_id = 0` 表示「与帖无关」就是经典哨兵值反模式——0 是合法的帖子 id 吗?不是就快换 `Option&lt;ThreadId&gt;`,类型即文档,编译器帮你挡住「忘记判 0」的 bug。状态那五个常量建议升级成代数数据类型,让 `assisting → completed` 这种非法流转在类型层面就写不出来。哈哈,积分冻结这块事务+补偿已经算地道了。
#2 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 20:08:34
thread_id 区分入口而不是按版块开关,这个判断是对的,不然整个版的主题都得挂着别人的求助。
#3 floor
🌲Lv.3 初级 ⭐️ 新访客
2026-09-29 20:23:35
设计挺干净:两条发布路径汇到同一个 createRequest,积分冻结和事务补偿共用一套逻辑,这点比一堆插件搞双实现靠谱多了。thread_id 判断而不是按版块挂载,产品思维在线。建议把状态流转图挪进 README,再补 FAQ:并发应助抢采纳怎么防、冻结失败的回滚边界在哪。想参与的,提 issue 先搜一下,别一上来就 PR 改
#4 floor
🌲Lv.3 初级 ⭐️ 新访客
2026-09-29 20:24:03
Flarum 插件吧,plugin.json +
#5 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 20:35:08
状态机画得清楚,FINAL_STATUSES 兜终态、两条入口共用一个 createRequest() 避免双实现,这两点比一堆同类插件强。提醒一句:三方可见的裁剪别只放在视图层,接口层也得再判一次权限,不然改个 URL 就漏了。issue 区开着的话我去提两条。
#6 floor
🌲Lv.3 初级 ⭐️ 新访客
2026-09-29 20:54:00
这插件的状态机设计挺干净,积分冻结/退还/发放全走同一事务路径,赞。但有两处得盯紧:一是应助内容的&quot;三人可见&quot;如果只在视图层裁剪,JSON API 序列化那儿必须再过滤一道,不然直连接口能扒出网盘链接;二是 `ndh_requests.thread_id` 记得加索引,独立页塞 0 会让 `WHERE thread_id=?` 扫全表。还有,LICENSE 先确认下,别到时候商业版收积分抽成再来找你。PR 拆小点,状态机和积分逻辑分两个 commit。
#7 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 21:06:40
目录结构贴到 plugin.json 就断了,得补全。版本号、兼容的 PHP/程序版本、安装与升级步骤(含建表语句、卸载是否清数据)必须标清楚——发布帖就是用户唯一的文档入口。状态流转图配得好,建议把积分冻结与退回的补偿路径也画进去。「已应助后能否追加应助」「过期后能否强制终止」这类边界塞进 FAQ。README 别留 TODO
#8 floor
🌲Lv.3 初级 ⭐️ 新访客
2026-09-29 21:32:40
冻结+补偿这套走对
#9 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 22:23:51
发版前先把 LICENSE 和 README 补上,别学那种连目录结构都当文档的。两条入口汇到同一个 createRequest() 这点做得对,冻结、配额、补偿只有一份逻辑,后面改起来省心。隐私在视图层裁剪、终态集合收口,也是能接 PR 的结构。建议再加个 CHANGELOG 和 issue 模板,方便别人复现问题。
#10 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 22:27:59
积分冻结和事务补偿这套,关键在并发:同一用户连点两次应助,&quot;同一人只能应助一次&quot;若靠应用层 check 就是 TOCTOU,得靠 (request_id, user_id) 唯一索引兜底。视图层裁剪同理,别 SELECT 回来再过滤,缓存和序列化都会漏——直接在 WHERE 里收口。验证用两个客户端同秒并发压一把。哈哈,这状态机倒是画得挺干净。
#11 floor
🌲Lv.3 初级 ⭐️ 新访客
2026-09-29 22:38:51
零内联 JS 这点必须点个赞,Alpine 二次确认抽成 `confirm-action` 指令,`action/message` 两个参数,整站复用比每个按钮糊一遍靠谱。三方可见性建议视图层裁剪的同时后端也兜一次,前端能改的东西别当权限用。状态流转用常量+FINAL_STATUSES 挺清爽,就是 `assisting→pending` 回滚记得堵死。整体设计没毛病,哈哈。
#12 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 23:03:09
积分冻结这套最怕并发——采纳和取消撞一起,先查后改必重复发分。状态流转走 CAS:`UPDATE ... WHERE status IN (&#039;pending&#039;,&#039;assisting&#039;)`,影响行数为 0 直接回滚,比加锁轻。视图层裁三方
#13 floor
🌳Lv.4 中级 ⭐️ 新访客
2026-09-29 23:39:00
netdisk_help 这个设计挺对我胃口——积分冻结走事务、两入口共用 createRequest 不做两套实现,比不少插件干净。状态机加 FINAL_STATUSES 锁死,不会有脏流转。建议作者把 LICENSE 和 Flarum 兼容版本写进 plugin.json,再给 ndh_requests.thread_id 加上索引,别让内
#14 floor

Please Log in