docs: add Phase 2 development log + research archive
- PHASE2_DEVELOPMENT_LOG.md: 完整交付物清单、修复记录、安全审计 - PHASE2_RESEARCH_ARCHIVE.md: Council 研究方向归档(BR-1~BR-5 / R-1~R-5 / FR-1~FR-5) - .gitignore: 排除 .worktrees/ 目录refactor/vr-ticket-20260416
parent
deacdedb01
commit
5dcf4286c8
|
|
@ -0,0 +1,6 @@
|
||||||
|
# git worktree metadata (do not commit worktrees)
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
|
# ShopXO runtime files
|
||||||
|
shopxo/runtime/
|
||||||
|
shopxo/vendor/ (if not committed)
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
# Phase 2 — 后台管理开发日志
|
||||||
|
|
||||||
|
> 状态:✅ 完成
|
||||||
|
> 时间:2026-04-15 04:36 — 14:30 CST
|
||||||
|
> 执行方式:Council 讨论(BackendArchitect + SecurityEngineer + FrontendDev,5轮)+ 西莉雅独立资料归档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
完成 vr-shopxo-plugin 后台管理页面开发,涵盖:
|
||||||
|
|
||||||
|
1. 座位模板管理(CRUD)
|
||||||
|
2. 电子票列表 / 详情 / 导出
|
||||||
|
3. 核销员管理(增删改查)
|
||||||
|
4. 核销记录查询
|
||||||
|
5. Admin 控制器鉴权修复(P1)
|
||||||
|
6. 审计日志(敏感操作记录)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 交付物清单
|
||||||
|
|
||||||
|
### 2.1 数据库层
|
||||||
|
|
||||||
|
| 表名 | 说明 | 关键字段 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `vr_seat_templates` | 座位模板 | name, category_id, seat_map, spec_base_id_map, status |
|
||||||
|
| `vr_tickets` | 电子票 | ticket_code, qr_data, verify_status, verifier_id, real_name/phone/id_card |
|
||||||
|
| `vr_verifiers` | 核销员 | user_id, name, status |
|
||||||
|
| `vr_verifications` | 核销记录 | ticket_id, verifier_id, verifier_name, goods_id, created_at |
|
||||||
|
| `vr_audit_log` | 审计日志(append-only) | action, operator_id, target_type, target_id, client_ip, user_agent, request_id, extra |
|
||||||
|
|
||||||
|
**索引:**
|
||||||
|
```sql
|
||||||
|
vr_tickets: KEY idx_goods_id(id), idx_ticket_code(ticket_code), idx_verify_status(verify_status)
|
||||||
|
vr_verifications: KEY idx_ticket_id(ticket_id), idx_verifier_id(verifier_id)
|
||||||
|
vr_audit_log: KEY idx_action, idx_operator_id, idx_target, idx_created_at
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 控制器层
|
||||||
|
|
||||||
|
| 文件 | 职责 | 关键方法 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `admin/controller/Base.php` | 鉴权基类(继承 Common) | `__construct()` → IsLogin + IsPower |
|
||||||
|
| `admin/controller/SeatTemplate.php` | 座位模板 CRUD | list / save / delete |
|
||||||
|
| `admin/controller/Ticket.php` | 电子票管理 | list / detail / verify / export |
|
||||||
|
| `admin/controller/Verifier.php` | 核销员管理 | list / save / delete |
|
||||||
|
| `admin/controller/Verification.php` | 核销记录查询 | list |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 服务层
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `service/BaseService.php` | 基础工具(table前缀/UUID/AES QR加密/AdminPowerMenu) |
|
||||||
|
| `service/TicketService.php` | 核销事务(含 FOR UPDATE 悲观锁) |
|
||||||
|
| `service/AuditService.php` | 审计日志(12种操作类型,append-only) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 视图层
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `admin/view/seat_template/list.html` | 座位模板列表 |
|
||||||
|
| `admin/view/seat_template/save.html` | 座位模板新增/编辑 |
|
||||||
|
| `admin/view/ticket/list.html` | 电子票列表(含搜索/筛选/导出) |
|
||||||
|
| `admin/view/ticket/detail.html` | 电子票详情 + QR码 + 手动核销 |
|
||||||
|
| `admin/view/verifier/list.html` | 核销员列表 |
|
||||||
|
| `admin/view/verifier/save.html` | 核销员新增/编辑 |
|
||||||
|
| `admin/view/verification/list.html` | 核销记录列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 关键修复记录
|
||||||
|
|
||||||
|
### P0:插件后台鉴权失败(卡死 4-5 小时的历史问题)
|
||||||
|
|
||||||
|
**根因:** 插件 `Base` 只调用 `AdminService::LoginInfo()`,跳过了 ShopXO `Common` 的完整鉴权链(`IsLogin()` + `IsPower()` + `FormTableInit()`)。
|
||||||
|
|
||||||
|
**修复:**
|
||||||
|
```php
|
||||||
|
// 修复前(错误)
|
||||||
|
class Base {
|
||||||
|
public function __construct() {
|
||||||
|
\app\service\AdminService::LoginInfo(); // 只填充 $this->admin,跳过权限检查
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复后(正确)
|
||||||
|
abstract class Base extends \app\admin\controller\Common {
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(); // 触发完整鉴权链
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复后鉴权链:**
|
||||||
|
```
|
||||||
|
ThinkPHP → admin.php → Common::__construct()
|
||||||
|
→ AdminService::LoginInfo() 填充 $this->admin
|
||||||
|
→ AdminPowerService::PowerMenuInit() 权限菜单
|
||||||
|
→ ViewInit() 视图初始化
|
||||||
|
→ 插件控制器(extends Base)
|
||||||
|
→ parent::__construct() → 自动获得上述所有鉴权
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1:Verifier.php CONCAT SQL 语法错误
|
||||||
|
|
||||||
|
**问题:** `column('CONCAT(nickname, "/", username)')` — ThinkPHP `column()` 不支持原始 SQL。
|
||||||
|
|
||||||
|
**修复:** 改用 `select()` + PHP 遍历拼接:
|
||||||
|
```php
|
||||||
|
$users_raw = Db::name('User')->where('id', 'in', $user_ids)->select();
|
||||||
|
$users = [];
|
||||||
|
foreach ($users_raw as $u) {
|
||||||
|
$users[$u['id']] = ($u['nickname'] ?: '') . '/' . ($u['username'] ?: '');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1:Verification.php column() 多字段映射 Bug
|
||||||
|
|
||||||
|
**问题:** `column('seat_info,real_name,goods_id', 'id')` — 返回结构与代码预期不符。
|
||||||
|
|
||||||
|
**修复:** 同样改用 `select()` + PHP 关联数组。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1:导出按钮 GET → POST
|
||||||
|
|
||||||
|
**问题:** `ticket/list.html` 的导出按钮是 `<a href>` GET 链接,但 `Ticket::export()` 要求 `IS_AJAX_POST`。
|
||||||
|
|
||||||
|
**修复:** 改为 `<button id="export-btn">`,JS 动态创建 POST form 提交:
|
||||||
|
```js
|
||||||
|
$('#export-btn').on('click', function() {
|
||||||
|
var $form = $('<form method="post" target="_blank">...');
|
||||||
|
$form.submit().remove();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1:TicketService::verifyTicket() 缺少事务保护
|
||||||
|
|
||||||
|
**问题:** 并发核销同一张票可能导致重复核销。
|
||||||
|
|
||||||
|
**修复:** 事务包裹 + `lock(true)` 悲观锁:
|
||||||
|
```php
|
||||||
|
Db::transaction(function() use ($ticket_code, $verifier_id) {
|
||||||
|
$ticket = Db::name('tickets')
|
||||||
|
->where('ticket_code', $ticket_code)
|
||||||
|
->lock(true) // = FOR UPDATE SKIP LOCKED
|
||||||
|
->find();
|
||||||
|
// 状态校验 + 更新 + 写入 vr_verifications
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2:Ticket::export() OOM 风险
|
||||||
|
|
||||||
|
**问题:** 大数据量时 `select()` 一次性加载所有记录到内存。
|
||||||
|
|
||||||
|
**修复:** 改用 `cursor()` 游标遍历:
|
||||||
|
```php
|
||||||
|
$rows = Db::name('plugins_vr_tickets')->where($where)->order('id', 'desc')->cursor();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 安全审计结果(S1-S5)
|
||||||
|
|
||||||
|
### S1 ✅ Admin 鉴权覆盖完整性
|
||||||
|
- `Base extends Common` 后,完整鉴权链自动继承
|
||||||
|
- 所有子控制器无需单独处理鉴权
|
||||||
|
|
||||||
|
### S2 ✅ SQL 注入无风险
|
||||||
|
- 所有输入均通过 ThinkPHP 查询构造器(自动绑定参数)
|
||||||
|
- 无原始 SQL 拼接
|
||||||
|
- `Verifier.php:45` / `Verification.php:55` CONCAT bug 已修复
|
||||||
|
|
||||||
|
### S3 ✅ XSS / CSRF 低风险
|
||||||
|
- CSRF Token 由 ShopXO 统一保护
|
||||||
|
- 关键操作(delete / verify / export)均有 `IS_AJAX_POST` 检查
|
||||||
|
- admin 上下文天然降低 XSS 风险
|
||||||
|
|
||||||
|
### S4 ✅ 审计日志设计完整
|
||||||
|
- `vr_audit_log` append-only 表
|
||||||
|
- 记录 action / operator_id / client_ip / user_agent / request_id / extra
|
||||||
|
- 4 索引优化查询性能
|
||||||
|
|
||||||
|
### S5 ✅ IDOR 水平越权
|
||||||
|
- admin 上下文(需登录 + 插件菜单权限)
|
||||||
|
- `Ticket::detail()` 无 owner check(admin 上下文已限制)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Phase 3 待办
|
||||||
|
|
||||||
|
- [ ] **FR-3**:座位图可视化编辑器(需产品需求确认 Canvas/SVG/Grid)
|
||||||
|
- [ ] **BR-2**:插件独立权限体系(核销员 vs 超级管理员菜单隔离)
|
||||||
|
- [ ] **R5 盲区**:IDOR 归属校验(核销员是否只能看自己的记录?)
|
||||||
|
- [ ] **BR-5**:核销通知队列化(think_queue)
|
||||||
|
- [ ] **FR-4**:CSV 导出携带搜索条件(keywords / verify_status)
|
||||||
|
- [ ] **FR-4**:Excel .xlsx 格式支持
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 经验教训
|
||||||
|
|
||||||
|
1. **插件 Base 必须继承 `Common`**:ShopXO 的鉴权链是 `Common` 构造方法的一部分,不能绕过
|
||||||
|
2. **ThinkPHP `column()` 不支持 CONCAT**:需用 `select()` + PHP 拼接
|
||||||
|
3. **导出按钮必须是 POST**:`IS_AJAX_POST` 检查存在,安全要求不能降级
|
||||||
|
4. **大量数据导出用 `cursor()`**:防止 OOM
|
||||||
|
5. **Council 超纲执行**:说"只讨论不动手"时 agents 仍会实现——需提前设 worktree 隔离
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
# Phase 2 — 研究资料归档
|
||||||
|
|
||||||
|
> 来源:Council 讨论(BackendArchitect + SecurityEngineer + FrontendDev,5轮)
|
||||||
|
> 整理:西莉雅(独立执行,非 Council 输出)
|
||||||
|
> 时间:2026-04-15 CST
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、后台架构研究(BR-1 ~ BR-5)
|
||||||
|
|
||||||
|
### BR-1:ThinkPHP 8 控制器层规范 ✅ 已验证
|
||||||
|
|
||||||
|
**Key Questions 结论:**
|
||||||
|
|
||||||
|
| 问题 | 答案 |
|
||||||
|
|------|------|
|
||||||
|
| ShopXO Admin 控制器基类 | `app\admin\controller\Common`,非 `BaseController` |
|
||||||
|
| 获取当前 admin id / role | `$this->admin['id']`,由 `Common::__construct()` 注入 |
|
||||||
|
| API 返回格式 | `\Json::成功()` / `\Json::fail()` 渲染器 |
|
||||||
|
| 分页参数规范 | `page` + `limit`(ThinkPHP 默认) |
|
||||||
|
| 新增 controller 路由 | 无需声明,ShopXO 自动扫描 |
|
||||||
|
|
||||||
|
**插件 Base 正确写法:**
|
||||||
|
```php
|
||||||
|
abstract class Base extends \app\admin\controller\Common {
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(); // 触发 IsLogin + IsPower + ViewInit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BR-2:多角色鉴权模型 ⚠️ 部分实现
|
||||||
|
|
||||||
|
| 问题 | 现状 | 待 Phase 3 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| ShopXO admin 角色结构 | `sx_admin` + `sx_admin_role`,RBAC | — |
|
||||||
|
| 核销员复用 admin 表? | **否**,独立表 `vr_verifiers` | — |
|
||||||
|
| 权限隔离方案 | 目前所有 admin 可访问所有插件功能 | 实现 RBAC 细化 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BR-3:数据库查询优化 ✅ 已实现
|
||||||
|
|
||||||
|
| 场景 | 解决方案 |
|
||||||
|
|------|---------|
|
||||||
|
| 大数据量导出 | `cursor()` 流式游标(防 OOM) |
|
||||||
|
| 模糊搜索 | ThinkPHP 查询构造器(自动绑定参数) |
|
||||||
|
| 分页 | `paginate(20)` |
|
||||||
|
| N+1 | `column()` / `select()` + PHP 拼接 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BR-4:核销操作事务一致性 ✅ 已实现
|
||||||
|
|
||||||
|
```php
|
||||||
|
Db::transaction(function() {
|
||||||
|
$ticket = Db::name('tickets')
|
||||||
|
->where('ticket_code', $ticket_code)
|
||||||
|
->lock(true) // FOR UPDATE SKIP LOCKED
|
||||||
|
->find();
|
||||||
|
// 状态校验 + 更新 + 写入 vr_verifications
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BR-5:事件驱动/异步队列 ❌ 待 Phase 3
|
||||||
|
|
||||||
|
- ShopXO 有 `think_queue`(Redis/DB)
|
||||||
|
- 核销通知同步执行暂无问题,队列化留 Phase 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、安全研究方向(R-1 ~ R-5)
|
||||||
|
|
||||||
|
### R1:Admin 控制器鉴权风险 ✅ 已解决
|
||||||
|
|
||||||
|
**根因:** 插件 `Base` 只调 `LoginInfo()`,未调用 `IsLogin()` + `IsPower()` + `FormTableInit()`。
|
||||||
|
|
||||||
|
**修复后鉴权链:**
|
||||||
|
```
|
||||||
|
ThinkPHP → admin.php → Common::__construct()
|
||||||
|
1. AdminService::LoginInfo() — 填充 $this->admin
|
||||||
|
2. AdminPowerService::PowerMenuInit() — 权限菜单
|
||||||
|
3. ViewInit() — 视图初始化
|
||||||
|
→ 插件控制器(extends Base)→ parent::__construct()
|
||||||
|
→ 完整继承上述 3 步
|
||||||
|
```
|
||||||
|
|
||||||
|
**Defense-in-Depth 建议:** 在 `Base::__construct()` 末尾显式调用 `$this->IsLogin()`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R2:SQL 注入风险 ✅ 无注入
|
||||||
|
|
||||||
|
| 控制器 | 查询点 | 输入处理 | 结论 |
|
||||||
|
|--------|--------|----------|------|
|
||||||
|
| SeatTemplate::list | `name` like, `status` | `null` + `intval()` | ✅ |
|
||||||
|
| Ticket::list | `keywords` multi-field like | `trim()` + 查询构造器绑定 | ✅ |
|
||||||
|
| Ticket::verify | `ticket_code`, `verifier_id` | `trim()` + `intval()` | ✅ |
|
||||||
|
| Verification::list | date range | `strtotime()` 绑定 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R3:XSS / CSRF ✅ 低风险
|
||||||
|
|
||||||
|
| 方面 | 状态 |
|
||||||
|
|------|------|
|
||||||
|
| CSRF Token (POST) | ✅ ShopXO 保护 |
|
||||||
|
| XSS(存储型) | ✅ 低风险(admin 上下文) |
|
||||||
|
| 关键操作 guard | ✅ `delete()` / `verify()` / `export()` 均有 `IS_AJAX_POST` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R4:敏感操作审计日志 ✅ 已实现
|
||||||
|
|
||||||
|
**vr_audit_log 字段:**
|
||||||
|
`action` / `operator_id` / `operator_name`(冗余) / `target_type` / `target_id` / `target_desc`(冗余) / `client_ip`(IPv6) / `user_agent` / `request_id` / `extra`(JSON快照) / `created_at`
|
||||||
|
|
||||||
|
**索引:** `idx_action` / `idx_operator_id` / `idx_target` / `idx_created_at`
|
||||||
|
|
||||||
|
**防篡改:** append-only 表,无 UPDATE/DELETE 接口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### R5:水平越权(IDOR)✅ Admin 上下文安全
|
||||||
|
|
||||||
|
**潜在盲区(待 Phase 3):**
|
||||||
|
- 核销员详情/编辑接口是否校验归属机构?
|
||||||
|
- 电子票详情接口是否校验持票人身份?
|
||||||
|
- 核销记录查询是否仅返回当前核销员/机构记录?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、前端研究方向(FR-1 ~ FR-5)
|
||||||
|
|
||||||
|
### FR-1:ShopXO Admin UI 框架 ✅ Layui
|
||||||
|
|
||||||
|
| 选项 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| Layui 2.x | ✅ 继续使用,零迁移成本 |
|
||||||
|
| Vue 3 | ❌ 不支持,混用会导致冲突 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### FR-2:ShopXO Admin 风格适配 ✅ 已适配
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 正确
|
||||||
|
PluginsAdminUrl('vr_ticket/ticket/list')
|
||||||
|
|
||||||
|
// ❌ 错误
|
||||||
|
MyUrl('ticket/list')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### FR-3:座位图编辑器 ⚠️ 待 Phase 3
|
||||||
|
|
||||||
|
- 需产品需求确认(Canvas/SVG/Grid?)
|
||||||
|
- `seat_map` JSON 字段已预留
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### FR-4:数据导出方案 ✅ CSV 已实现,.xlsx 待 Phase 3
|
||||||
|
|
||||||
|
| 项目 | 现状 |
|
||||||
|
|------|------|
|
||||||
|
| CSV 导出 | ✅ `cursor()` 流式,防 OOM |
|
||||||
|
| 导出按钮 | ✅ POST-only form |
|
||||||
|
| Excel .xlsx | ❌ 待 Phase 3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### FR-5:响应式与权限控制 ⚠️ 部分实现
|
||||||
|
|
||||||
|
- 权限体系复用 ShopXO RBAC
|
||||||
|
- 移动端 PAD 核销场景待 Phase 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Phase 3 待办清单
|
||||||
|
|
||||||
|
### 需产品确认
|
||||||
|
- [ ] FR-3:座位图可视化编辑器需求
|
||||||
|
- [ ] FR-5:PAD 核销场景是否需要独立移动端页面?
|
||||||
|
- [ ] BR-2:核销员账号体系(复用 admin 还是独立?)
|
||||||
|
|
||||||
|
### 技术待办
|
||||||
|
- [ ] Defense-in-Depth:`Base::__construct()` 末尾显式调用 `IsLogin()`
|
||||||
|
- [ ] R5 盲区验证:IDOR 归属校验
|
||||||
|
- [ ] BR-5:核销通知队列化(think_queue)
|
||||||
|
- [ ] FR-4:CSV 导出携带搜索条件
|
||||||
|
- [ ] FR-4:Excel .xlsx 格式支持
|
||||||
|
- [ ] BR-2:插件独立权限(菜单级/操作级)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、关键文件索引
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `app/plugins/vr_ticket/admin/controller/Base.php` | 鉴权基类(已修复) |
|
||||||
|
| `app/plugins/vr_ticket/service/AuditService.php` | 审计日志服务 |
|
||||||
|
| `app/plugins/vr_ticket/service/TicketService.php` | 核销事务(含 FOR UPDATE) |
|
||||||
|
| `app/plugins/vr_ticket/admin/controller/Ticket.php` | 电子票 CRUD + export |
|
||||||
|
| `app/plugins/vr_ticket/admin/controller/Verifier.php` | 核销员管理 |
|
||||||
|
| `app/plugins/vr_ticket/EventListener.php` | vr_audit_log 建表 SQL |
|
||||||
|
| `shopxo/app/admin/controller/Common.php` | ShopXO 鉴权链源码 |
|
||||||
Loading…
Reference in New Issue