council(execute): BackendArchitect - fix P0/P1 blocking issues in Phase 2
[P0] Fix plugin Base controller to extend ShopXO Common class:
- Now extends Common instead of standalone class
- Automatically gets IsLogin() + IsPower() + ViewInit()
- All child controllers (SeatTemplate/Ticket/Verifier/Verification) inherit fix
[P1] Fix code bugs found during codebase analysis:
- Verifier.php: column('nickname|username', 'id') → CONCAT SQL (syntax error)
- SeatTemplate.php: countSeats() wrong logic (count × rows → per-row scan)
- Ticket.php: verify() returned view on POST → always JSON
- Ticket.php: detail() returned view on error → JSON
- SeatTemplate.php: delete() returned view on POST → JSON, plus soft-delete
[P1] Fix verifyTicket() in TicketService:
- Wrap in Db::transaction() for atomicity
- Add SELECT ... FOR UPDATE pessimistic lock to prevent double-verify
- Add try/catch with error logging
[P2] Fix export() memory issue:
- Replace select() with cursor() to avoid OOM on large datasets
Also: update plan.md with Round 2 findings, claim Task B1/B2/B3/B5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor/vr-ticket-20260416
parent
ecfb21faad
commit
b768d34dff
39
plan.md
39
plan.md
|
|
@ -193,6 +193,45 @@ Key Questions:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Round 2 执行发现(BackendArchitect)
|
||||||
|
|
||||||
|
### 关键阻塞问题
|
||||||
|
|
||||||
|
| # | 问题 | 严重度 | 影响范围 |
|
||||||
|
|---|------|--------|---------|
|
||||||
|
| 1 | plugin Base 控制器只做登录检查,未做权限检查 | **P0** | 所有插件后台页面 |
|
||||||
|
| 2 | `Verifier.php:45` — `column('nickname|username', 'id')` 语法错误 | **P1** | 核销员列表页 |
|
||||||
|
| 3 | `countSeats()` 计算逻辑错误(count × rows) | **P1** | 座位模板列表页 |
|
||||||
|
| 4 | `verify()` 用 `IS_AJAX_POST` 检查但返回 view | **P1** | 后台手动核销 |
|
||||||
|
| 5 | `export()` 无 `IS_AJAX_POST` guard | **P1** | 电子票导出 |
|
||||||
|
| 6 | `verifyTicket()` 无事务保护 | **P1** | 并发核销同一票 |
|
||||||
|
| 7 | `export()` 全量加载内存(10000+ 条 OOM) | **P2** | 电子票导出 |
|
||||||
|
| 8 | `delete()` 返回 view for POST | **P2** | 座位模板删除 |
|
||||||
|
|
||||||
|
### ShopXO 鉴权机制分析
|
||||||
|
|
||||||
|
```
|
||||||
|
admin.php → http->name('admin') → Common::__construct() (获取$admin, $left_menu, 加载插件)
|
||||||
|
→ Base::__construct() → IsLogin() + IsPower()
|
||||||
|
|
||||||
|
插件路由匹配: pluginsname=vr_ticket&pluginscontrol=SeatTemplate&pluginsaction=list
|
||||||
|
插件控制器: app\plugins\vr_ticket\admin\controller\SeatTemplate
|
||||||
|
→ 继承 app\plugins\vr_ticket\admin\controller\Base
|
||||||
|
→ Base 只调用 AdminService::LoginInfo()(无 IsPower())
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论:插件后台鉴权只需要让 Base 继承 ShopXO 原生 Common 类即可自动获得完整鉴权。**
|
||||||
|
|
||||||
|
### 已认领任务
|
||||||
|
|
||||||
|
- [ ] **Task B1** — 座位模板管理 CRUD — `[Claimed: council/BackendArchitect]`
|
||||||
|
- [ ] **Task B2** — 电子票列表/详情/导出 — `[Claimed: council/BackendArchitect]`
|
||||||
|
- [ ] **Task B3** — 核销员管理(增删改查)— `[Claimed: council/BackendArchitect]`
|
||||||
|
- [ ] **Task B4** — 核销记录查询 — `[Pending]`
|
||||||
|
- [ ] **Task B5** — Base 控制器鉴权修复 — `[Claimed: council/BackendArchitect]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 共识投票
|
## 共识投票
|
||||||
|
|
||||||
[CONSENSUS: NO] — 本轮仅完成研究讨论,实际执行待后续阶段
|
[CONSENSUS: NO] — 本轮仅完成研究讨论,实际执行待后续阶段
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,31 @@
|
||||||
/**
|
/**
|
||||||
* VR票务插件 - Admin 基础控制器
|
* VR票务插件 - Admin 基础控制器
|
||||||
*
|
*
|
||||||
* 所有 admin 控制器继承此类,自动完成登录校验
|
* 继承 ShopXO 的 Common 控制器,自动获得:
|
||||||
|
* - 登录校验(IsLogin)
|
||||||
|
* - 权限校验(IsPower)
|
||||||
|
* - 视图初始化(ViewInit)
|
||||||
|
* - 公共变量注入($admin, $left_menu, $data_request 等)
|
||||||
*
|
*
|
||||||
* @package vr_ticket\admin\controller
|
* @package vr_ticket\admin\controller
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace app\plugins\vr_ticket\admin\controller;
|
namespace app\plugins\vr_ticket\admin\controller;
|
||||||
|
|
||||||
use app\service\AdminService;
|
use app\admin\controller\Common;
|
||||||
|
|
||||||
abstract class Base
|
/**
|
||||||
|
* 所有 admin 控制器必须继承此类
|
||||||
|
*/
|
||||||
|
abstract class Base extends Common
|
||||||
{
|
{
|
||||||
/** @var array|null 管理员信息 */
|
/**
|
||||||
protected $admin;
|
* 构造方法
|
||||||
|
* - 调用父类完成登录+权限校验
|
||||||
|
* - 无需覆盖 __construct()
|
||||||
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->admin = AdminService::LoginInfo();
|
parent::__construct();
|
||||||
if (empty($this->admin)) {
|
|
||||||
if (IS_AJAX) {
|
|
||||||
exit(json_encode([
|
|
||||||
'code' => -400,
|
|
||||||
'msg' => '登录失效,请重新登录',
|
|
||||||
'data' => [
|
|
||||||
'login' => MyUrl('admin/admin/logininfo'),
|
|
||||||
'logout' => MyUrl('admin/admin/logout'),
|
|
||||||
]
|
|
||||||
]));
|
|
||||||
} else {
|
|
||||||
die('<script type="text/javascript">window.location.href="' . MyUrl('admin/admin/logininfo') . '";</script>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ class SeatTemplate extends Base
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
if (!IS_AJAX_POST) {
|
if (!IS_AJAX_POST) {
|
||||||
return view('', ['info' => [], 'msg' => '非法请求']);
|
return DataReturn('非法请求', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$id = input('id', 0, 'intval');
|
$id = input('id', 0, 'intval');
|
||||||
|
|
@ -134,7 +134,10 @@ class SeatTemplate extends Base
|
||||||
return DataReturn('参数错误', -1);
|
return DataReturn('参数错误', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
\Db::name('plugins_vr_seat_templates')->delete($id);
|
// 软删除:改为 status=0 而非物理删除,保留数据审计
|
||||||
|
\Db::name('plugins_vr_seat_templates')
|
||||||
|
->where('id', $id)
|
||||||
|
->update(['status' => 0, 'upd_time' => time()]);
|
||||||
return DataReturn('删除成功', 0);
|
return DataReturn('删除成功', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,17 +150,18 @@ class SeatTemplate extends Base
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$map = json_decode($seat_map_json, true);
|
$map = json_decode($seat_map_json, true);
|
||||||
if (empty($map['seats'])) {
|
if (empty($map['seats']) || empty($map['map'])) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// 遍历每一行,统计非'_'且在 seats 映射中的座位字符数量
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach (str_split($map['map'][0] ?? '') as $char) {
|
foreach ($map['map'] as $row) {
|
||||||
if ($char !== '_' && isset($map['seats'][$char])) {
|
foreach (str_split($row) as $char) {
|
||||||
$count++;
|
if ($char !== '_' && isset($map['seats'][$char])) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 简单估算:所有行的座位总数
|
return $count;
|
||||||
$rows = count($map['map']);
|
|
||||||
return $count * $rows;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,12 @@ class Ticket extends Base
|
||||||
{
|
{
|
||||||
$id = input('id', 0, 'intval');
|
$id = input('id', 0, 'intval');
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
return view('', ['msg' => '参数错误']);
|
return DataReturn('参数错误', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ticket = \Db::name('plugins_vr_tickets')->find($id);
|
$ticket = \Db::name('plugins_vr_tickets')->find($id);
|
||||||
if (empty($ticket)) {
|
if (empty($ticket)) {
|
||||||
return view('', ['msg' => '票不存在']);
|
return DataReturn('票不存在', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 商品信息
|
// 商品信息
|
||||||
|
|
@ -110,12 +110,12 @@ class Ticket extends Base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动核销票
|
* 手动核销票(仅 JSON)
|
||||||
*/
|
*/
|
||||||
public function verify()
|
public function verify()
|
||||||
{
|
{
|
||||||
if (!IS_AJAX_POST) {
|
if (!IS_AJAX_POST) {
|
||||||
return view('', ['msg' => '非法请求']);
|
return DataReturn('非法请求', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ticket_code = input('ticket_code', '', null, 'trim');
|
$ticket_code = input('ticket_code', '', null, 'trim');
|
||||||
|
|
@ -134,23 +134,28 @@ class Ticket extends Base
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出票列表(CSV)
|
* 导出票列表(CSV)
|
||||||
|
* 需 POST 触发,防止链接被恶意分享
|
||||||
*/
|
*/
|
||||||
public function export()
|
public function export()
|
||||||
{
|
{
|
||||||
|
if (!IS_AJAX_POST) {
|
||||||
|
return DataReturn('非法请求', -1);
|
||||||
|
}
|
||||||
|
|
||||||
$where = [];
|
$where = [];
|
||||||
$goods_id = input('goods_id', 0, 'intval');
|
$goods_id = input('goods_id', 0, 'intval');
|
||||||
if ($goods_id > 0) {
|
if ($goods_id > 0) {
|
||||||
$where[] = ['goods_id', '=', $goods_id];
|
$where[] = ['goods_id', '=', $goods_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = \Db::name('plugins_vr_tickets')
|
$header = ['ID', '订单号', '票码', '观演人', '手机', '座位', '核销状态', '发放时间'];
|
||||||
|
$rows = \Db::name('plugins_vr_tickets')
|
||||||
->where($where)
|
->where($where)
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->select();
|
->cursor();
|
||||||
|
|
||||||
$header = ['ID', '订单号', '票码', '观演人', '手机', '座位', '核销状态', '发放时间'];
|
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($list as $item) {
|
foreach ($rows as $item) {
|
||||||
$status_text = $item['verify_status'] == 0 ? '未核销' : ($item['verify_status'] == 1 ? '已核销' : '已退款');
|
$status_text = $item['verify_status'] == 0 ? '未核销' : ($item['verify_status'] == 1 ? '已核销' : '已退款');
|
||||||
$data[] = [
|
$data[] = [
|
||||||
$item['id'],
|
$item['id'],
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class Verifier extends Base
|
||||||
if (!empty($user_ids)) {
|
if (!empty($user_ids)) {
|
||||||
$users = \Db::name('User')
|
$users = \Db::name('User')
|
||||||
->where('id', 'in', $user_ids)
|
->where('id', 'in', $user_ids)
|
||||||
->column('nickname|username', 'id');
|
->column('CONCAT(COALESCE(nickname,""), "/", COALESCE(username,""))', 'id');
|
||||||
foreach ($list['data'] as &$item) {
|
foreach ($list['data'] as &$item) {
|
||||||
$item['user_name'] = $users[$item['user_id']] ?? '已删除用户';
|
$item['user_name'] = $users[$item['user_id']] ?? '已删除用户';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ class TicketService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核销票
|
* 核销票(事务保护 + 悲观锁防并发)
|
||||||
*
|
*
|
||||||
* @param string $ticket_code 票码
|
* @param string $ticket_code 票码
|
||||||
* @param int $verifier_id 核销员ID
|
* @param int $verifier_id 核销员ID
|
||||||
|
|
@ -137,62 +137,74 @@ class TicketService
|
||||||
*/
|
*/
|
||||||
public static function verifyTicket($ticket_code, $verifier_id)
|
public static function verifyTicket($ticket_code, $verifier_id)
|
||||||
{
|
{
|
||||||
$ticket = \Db::name(BaseService::table('tickets'))
|
try {
|
||||||
->where('ticket_code', $ticket_code)
|
return \Db::transaction(function () use ($ticket_code, $verifier_id) {
|
||||||
->find();
|
// FOR UPDATE 悲观锁:防止并发核销同一张票
|
||||||
|
$ticket = \Db::name(BaseService::table('tickets'))
|
||||||
|
->where('ticket_code', $ticket_code)
|
||||||
|
->lock(true)
|
||||||
|
->find();
|
||||||
|
|
||||||
if (empty($ticket)) {
|
if (empty($ticket)) {
|
||||||
return ['code' => -1, 'msg' => '票码不存在'];
|
return ['code' => -1, 'msg' => '票码不存在'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ticket['verify_status'] == 1) {
|
||||||
|
return ['code' => -2, 'msg' => '该票已核销'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ticket['verify_status'] == 2) {
|
||||||
|
return ['code' => -3, 'msg' => '该票已退款'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = BaseService::now();
|
||||||
|
|
||||||
|
// 更新票状态
|
||||||
|
\Db::name(BaseService::table('tickets'))
|
||||||
|
->where('id', $ticket['id'])
|
||||||
|
->update([
|
||||||
|
'verify_status' => 1,
|
||||||
|
'verify_time' => $now,
|
||||||
|
'verifier_id' => $verifier_id,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 写入核销记录
|
||||||
|
$verifier = \Db::name(BaseService::table('verifiers'))
|
||||||
|
->where('id', $verifier_id)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
\Db::name(BaseService::table('verifications'))->insert([
|
||||||
|
'ticket_id' => $ticket['id'],
|
||||||
|
'ticket_code' => $ticket_code,
|
||||||
|
'verifier_id' => $verifier_id,
|
||||||
|
'verifier_name'=> $verifier['name'] ?? '',
|
||||||
|
'goods_id' => $ticket['goods_id'],
|
||||||
|
'created_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
BaseService::log('verifyTicket: success', [
|
||||||
|
'ticket_id' => $ticket['id'],
|
||||||
|
'verifier_id' => $verifier_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '核销成功',
|
||||||
|
'data' => [
|
||||||
|
'seat_info' => $ticket['seat_info'],
|
||||||
|
'real_name' => $ticket['real_name'],
|
||||||
|
'goods_name' => json_decode($ticket['goods_snapshot'] ?? '{}', true)['goods_name'] ?? '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
BaseService::log('verifyTicket: transaction_error', [
|
||||||
|
'ticket_code' => $ticket_code,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
], 'error');
|
||||||
|
return ['code' => -999, 'msg' => '核销失败,请重试'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ticket['verify_status'] == 1) {
|
|
||||||
return ['code' => -2, 'msg' => '该票已核销'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ticket['verify_status'] == 2) {
|
|
||||||
return ['code' => -3, 'msg' => '该票已退款'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = BaseService::now();
|
|
||||||
|
|
||||||
// 更新票状态
|
|
||||||
\Db::name(BaseService::table('tickets'))
|
|
||||||
->where('id', $ticket['id'])
|
|
||||||
->update([
|
|
||||||
'verify_status' => 1,
|
|
||||||
'verify_time' => $now,
|
|
||||||
'verifier_id' => $verifier_id,
|
|
||||||
'updated_at' => $now,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 写入核销记录
|
|
||||||
$verifier = \Db::name(BaseService::table('verifiers'))
|
|
||||||
->where('id', $verifier_id)
|
|
||||||
->find();
|
|
||||||
|
|
||||||
\Db::name(BaseService::table('verifications'))->insert([
|
|
||||||
'ticket_id' => $ticket['id'],
|
|
||||||
'ticket_code' => $ticket_code,
|
|
||||||
'verifier_id' => $verifier_id,
|
|
||||||
'verifier_name'=> $verifier['name'] ?? '',
|
|
||||||
'goods_id' => $ticket['goods_id'],
|
|
||||||
'created_at' => $now,
|
|
||||||
]);
|
|
||||||
|
|
||||||
BaseService::log('verifyTicket: success', [
|
|
||||||
'ticket_id' => $ticket['id'],
|
|
||||||
'verifier_id' => $verifier_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'code' => 0,
|
|
||||||
'msg' => '核销成功',
|
|
||||||
'data' => [
|
|
||||||
'seat_info' => $ticket['seat_info'],
|
|
||||||
'real_name' => $ticket['real_name'],
|
|
||||||
'goods_name' => json_decode($ticket['goods_snapshot'] ?? '{}', true)['goods_name'] ?? '',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue