diff --git a/.gitignore b/.gitignore index b70e211..452c557 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ shopxo/public/adminufgeyw.php # 强制追踪 vr_ticket 插件的 database 目录(全局 gitignore 的 database 规则过宽) !shopxo/app/plugins/vr_ticket/database/ + +#插件目录 +.agent/ +.claude/ +.gitnexus/ +graphify-out/ diff --git a/docs/DEVELOPMENT_LOG.md b/docs/DEVELOPMENT_LOG.md index 3d4da50..c332ec9 100644 --- a/docs/DEVELOPMENT_LOG.md +++ b/docs/DEVELOPMENT_LOG.md @@ -778,3 +778,88 @@ if (empty($room['id'])) { - ✅ antigravity 测试通过(基本没问题) - ✅ 西莉雅代码审查通过(读取优先级 + 防御层双重保障) - ✅ 多模板模式修复验证 + +--- + +## 2026-04-25 至 2026-04-27 B端核销冲刺(周末完成) + +### 分支状态 +- 分支:`feat/b-verification-clean`(从 `feat/b-verification` 合并而来) +- HEAD:`cd0e70e9d remove: 座位模板页面和控制器(被场馆配置覆盖)` +### 本次提交记录(a873aac14 → cd0e70e9d) + +| Commit | 说明 | +|--------|------| +| `cd0e70e9d` | remove: 座位模板页面和控制器(被场馆配置覆盖) | +| `a5b2d00c8` | fix(admin): visitor_name→real_name, verify_status, TicketVerify GET handler, input filter null→s | +| `6fae40698` | docs: 强化经验#4 — 插件视图路径 view/{group}/ 结构图解 | +| `51bcc207f` | fix(admin): MyView路径 admin/view→view/admin(7处修正) | +| `6c18dd38f` | docs: 追加经验#19 — AdminSidebarInit items vs item 属性名陷阱 | +| `44553442e` | fix(admin): 修复菜单注册项属性名 item→items(ShopXO sidebar 渲染器期望 items 复数) | +| `23d2b2f7b` | fix(B): TicketVerify M-05修复(verifier_id从session) + 新增TicketStats API | +| `c93cc1134` | feat(B): B端核销功能合入(session key + $this->admin 修正) | +| `a873aac14` | docs: Phase B plan + README status update | + +### 未提交变更(Working Directory) + +> 用户要求只写文档不修改代码,以下变更在本地工作区,尚未 commit。 + +**Admin.php(input 过滤器 + 短码自动识别):** +- `input('xxx', '', null, 'trim')` → `input('xxx', '', '', 'trim')` 修复 null→空字符串问题 +- TicketVerify 新增 `verifyByShortCode()` 分支逻辑:长度 < 20 且不含连字符视为短码 + +**视图 UI 统一升级(5个文件):** +- 所有 admin 页面统一改为 `.page-title` 大标题风格(icon + 标题 + 底部分隔线) +- ticket/list.html:搜索表单改为 POST + `form-validation` + 搜索按钮 loading 状态 + 重置按钮 +- ticket/verify.html:**核心重写** — 统计栏初始值 `-`(不再显示「加载中...」)+ form-validation submit-ajax 模式 + ShopXO 原生 submit-ajax 回调 +- verification/list.html:搜索表单 POST 化 + 表格包裹 panel + 统计条数 +- verifier/list.html:搜索表单 POST 化 + 启用/禁用改用 ShopXO `submit-ajax`(不再用 onclick 手动 ajax) +- setup.html:UI 标题更新 +- venue/list.html:移除顶部「插件全局设置」工具栏按钮 + +### Phase B 完成度 + +| 子项 | 状态 | 说明 | +|------|------|------| +| B0-1 Admin.php 权限校验 | ✅ 完成 | session('admin_id') 校验 | +| B0-2 verifier_id session来源 | ✅ 完成 | M-05 已修复 | +| B0-3 ALTER TABLE bug | ✅ 完成 | B端合入时已修 | +| B1-1 扫码核销页面 | ✅ 可用 | verify.html 基本可用,大量细节待修 | +| B1-2 电子票列表 | ✅ 可用 | list.html 已可用 | +| B1-3 票详情 | ✅ 可用 | detail.html 已生成 | +| B2-1 核销员管理 | ✅ 可用 | verifier/list.html + save.html | +| B2-2 核销记录 | ✅ 可用 | verification/list.html | +| B2-3 座位模板管理 | ✅ 已移除 | 被场馆配置覆盖,无需独立页面 | + +### 关键修复记录 + +**TicketVerify 短码自动识别(Admin.php):** +```php +$is_short_code = (strlen($ticket_code) < 20 && strpos($ticket_code, '-') === false); +if ($is_short_code) { + $result = TicketService::verifyByShortCode($ticket_code, $verifier_id); +} else { + $result = TicketService::verifyTicket($ticket_code, $verifier_id); +} +``` + +**ShopXO form-validation submit-ajax(verify.html):** +- 原:手动 `$.ajax()` + `$('#verify-form').on('submit', e.preventDefault)` +- 新:`
` + `$(document).on('submit-success.form-validation')` +- ShopXO 自动处理:loading 状态 / 错误提示 / 成功后回调 + +### 待修复细节(已知问题) + +| 问题 | 优先级 | 说明 | +|------|--------|------| +| verify.html 扫码摄像头 | 🟡 中 | 摄像头调用代码已写,但无二维码解析库(jsQR),扫码后无法自动填入输入框 | +| verification/list.html 日期筛选 | 🟡 中 | datepicker 可能未初始化 | +| verifier/save.html | 🟡 中 | 添加核销员表单未确认 | +| ticket/detail.html QR码显示 | 🟡 中 | detail 页面已生成,QR码渲染待验证 | + +### 文档同步 + +- Issue #6(安全-P0):M-01/M-02/M-05/M-06 已修完,待关闭 +- Issue #7(安全-P1):M-08 等项待审阅确认 +- Issue #22:Phase 4/Phase 2 状态已更新 +- EXPERIENCES.md:新增经验条目 #4(视图路径)、#19(items 属性名) diff --git a/shopxo/app/plugins/vr_ticket/admin/Admin.php b/shopxo/app/plugins/vr_ticket/admin/Admin.php index 70e4e8b..82cd6be 100644 --- a/shopxo/app/plugins/vr_ticket/admin/Admin.php +++ b/shopxo/app/plugins/vr_ticket/admin/Admin.php @@ -123,23 +123,23 @@ class Admin extends Common $where = []; // 逐字段精确搜索(对应视图的独立搜索框) - $order_no = input('order_no', '', null, 'trim'); + $order_no = input('order_no', '', '', 'trim'); if ($order_no !== '') { $where[] = ['order_no', 'like', "%{$order_no}%"]; } - $ticket_code = input('ticket_code', '', 's', 'trim'); + $ticket_code = input('ticket_code', '', '', 'trim'); if ($ticket_code !== '') { $where[] = ['ticket_code', 'like', "%{$ticket_code}%"]; } - $real_name = input('real_name', '', null, 'trim'); + $real_name = input('real_name', '', '', 'trim'); if ($real_name !== '') { $where[] = ['real_name', 'like', "%{$real_name}%"]; } - $phone = input('phone', '', null, 'trim'); + $phone = input('phone', '', '', 'trim'); if ($phone !== '') { $where[] = ['phone', 'like', "%{$phone}%"]; } - $verify_status = input('verify_status', '', null); + $verify_status = input('verify_status', '', '', 'trim'); if ($verify_status !== '' && $verify_status !== null) { $where[] = ['verify_status', '=', intval($verify_status)]; } @@ -237,7 +237,7 @@ class Admin extends Common return json(['code' => -1, 'msg' => '非法请求']); } - $ticket_code = input('ticket_code', '', 's', 'trim'); + $ticket_code = input('ticket_code', '', '', 'trim'); // M-05: verifier_id 从 session 获取,禁止客户端伪造 // $this->admin 来自父类构造函数:AdminService::LoginInfo() @@ -255,7 +255,17 @@ class Admin extends Common return DataReturn('票码不能为空', -1); } - $result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id); + // 自动识别短码 vs UUID:短码长度 < 20 且不含连字符 + $is_short_code = (strlen($ticket_code) < 20 && strpos($ticket_code, '-') === false); + + if ($is_short_code) { + // 短码核销 + $result = \app\plugins\vr_ticket\service\TicketService::verifyByShortCode($ticket_code, $verifier_id); + } else { + // UUID 核销 + $result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id); + } + return DataReturn($result['msg'], $result['code'], $result['data'] ?? []); } diff --git a/shopxo/app/plugins/vr_ticket/service/BaseService.php b/shopxo/app/plugins/vr_ticket/service/BaseService.php index 3453c27..58db9cf 100644 --- a/shopxo/app/plugins/vr_ticket/service/BaseService.php +++ b/shopxo/app/plugins/vr_ticket/service/BaseService.php @@ -489,8 +489,8 @@ class BaseService public static function signQrPayload(array $payload): string { $secret = self::getVrSecret(); - // 签名内容:id.g.iat.exp - $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; + // 签名内容:code.id.g.iat.exp(code = ticket_code) + $sign_str = $payload['code'] . '.' . $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); $payload['sig'] = $sig; @@ -515,8 +515,8 @@ class BaseService return null; } - // 必填字段检查 - if (!isset($payload['id'], $payload['g'], $payload['iat'], $payload['exp'], $payload['sig'])) { + // 必填字段检查(包含 ticket_code) + if (!isset($payload['id'], $payload['g'], $payload['code'], $payload['iat'], $payload['exp'], $payload['sig'])) { return null; } @@ -525,9 +525,9 @@ class BaseService return null; } - // 签名验证 + // 签名验证(包含 ticket_code) $secret = self::getVrSecret(); - $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; + $sign_str = $payload['code'] . '.' . $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); if (!hash_equals($expected_sig, $payload['sig'])) { @@ -535,9 +535,10 @@ class BaseService } return [ - 'id' => intval($payload['id']), - 'g' => intval($payload['g']), - 'exp' => intval($payload['exp']), + 'id' => intval($payload['id']), + 'g' => intval($payload['g']), + 'code' => $payload['code'], + 'exp' => intval($payload['exp']), ]; } } diff --git a/shopxo/app/plugins/vr_ticket/service/TicketService.php b/shopxo/app/plugins/vr_ticket/service/TicketService.php index 4fa7d54..028b187 100644 --- a/shopxo/app/plugins/vr_ticket/service/TicketService.php +++ b/shopxo/app/plugins/vr_ticket/service/TicketService.php @@ -187,10 +187,11 @@ class TicketService extends BaseService // 短码存储在 qr_data 中,供前端展示 $short_code = BaseService::shortCodeEncode($og['goods_id'], $ticket_id); - // Step 3: 生成 QR payload(HMAC-SHA256 签名,30分钟有效) + // Step 3: 生成 QR payload(HMAC-SHA256 签名,30分钟有效,含 ticket_code) $qr_payload = BaseService::signQrPayload([ 'id' => $ticket_id, 'g' => $og['goods_id'], + 'code' => $ticket_code, 'iat' => $now, 'exp' => $now + 1800, // 30分钟 ]); @@ -516,16 +517,22 @@ class TicketService extends BaseService $decoded = BaseService::verifyQrPayload($payload); if ($decoded !== null && $decoded['exp'] - time() > 900) { // 有效期 > 15分钟,返回缓存 - return [ - 'code' => 0, - 'msg' => 'success', - 'data' => [ - 'short_code' => $short_code, - 'payload' => $payload, - 'cached' => true, - 'expires_in' => $decoded['exp'] - time(), - ], - ]; + // 签名验证(包含 ticket_code) + $secret = self::getVrSecret(); + $sign_str = $decoded['code'] . '.' . $decoded['id'] . '.' . $decoded['g'] . '.' . $decoded['iat'] . '.' . $decoded['exp']; + $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); + if (isset($decoded['sig']) && $decoded['sig'] === $expected_sig) { + return [ + 'code' => 0, + 'msg' => 'success', + 'data' => [ + 'short_code' => $short_code, + 'payload' => $payload, + 'cached' => true, + 'expires_in' => $decoded['exp'] - time(), + ], + ]; + } } } diff --git a/shopxo/app/plugins/vr_ticket/service/WalletService.php b/shopxo/app/plugins/vr_ticket/service/WalletService.php index e86b60a..e7051d3 100644 --- a/shopxo/app/plugins/vr_ticket/service/WalletService.php +++ b/shopxo/app/plugins/vr_ticket/service/WalletService.php @@ -196,10 +196,11 @@ class WalletService extends BaseService $expiresAt = $now + self::QR_TTL; $payload = [ - 'id' => $ticket['id'], - 'g' => $ticket['goods_id'], - 'iat' => $now, - 'exp' => $expiresAt, + 'id' => $ticket['id'], + 'g' => $ticket['goods_id'], + 'code' => $ticket['ticket_code'], + 'iat' => $now, + 'exp' => $expiresAt, ]; $encoded = self::signQrPayload($payload); diff --git a/shopxo/app/plugins/vr_ticket/view/admin/setup.html b/shopxo/app/plugins/vr_ticket/view/admin/setup.html index 784449b..c3b73d9 100644 --- a/shopxo/app/plugins/vr_ticket/view/admin/setup.html +++ b/shopxo/app/plugins/vr_ticket/view/admin/setup.html @@ -1,11 +1,19 @@ {{:ModuleInclude('public/header')}} + +
-
- - 返回 - - 插件设置 +
+ 插件设置
diff --git a/shopxo/app/plugins/vr_ticket/view/admin/ticket/list.html b/shopxo/app/plugins/vr_ticket/view/admin/ticket/list.html index a0ccb90..048ba8b 100644 --- a/shopxo/app/plugins/vr_ticket/view/admin/ticket/list.html +++ b/shopxo/app/plugins/vr_ticket/view/admin/ticket/list.html @@ -1,40 +1,56 @@ {{:ModuleInclude('public/header')}} + +
-
- - 扫码核销 - - 电子票列表 +
+ 电子票列表
-
+
- +
订单号 - +
票码 - +
观演人 - +
手机号 - +
@@ -45,17 +61,22 @@ 状态
- - + 重置
@@ -82,33 +103,35 @@ {{if !empty($list)}} - {{volist name="list" id="ticket"}} - - {{$ticket.ticket_code}} - {{$ticket.real_name|default='—'}} - {{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}} - {{$ticket.goods_title|default='已删除商品'}} - - {{if $ticket.verify_status == 0}} - 未核销 - {{elseif $ticket.verify_status == 1}} - 已核销 - {{elseif $ticket.verify_status == 2}} - 已退款 - {{/if}} - - {{if !empty($ticket.issued_at)}}{{:date('Y-m-d H:i', $ticket.issued_at)}}{{else}}—{{/if}} - - - 查看详情 - - - - {{/volist}} + {{volist name="list" id="ticket"}} + + {{$ticket.ticket_code}} + {{$ticket.real_name|default='—'}} + {{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}} + {{$ticket.goods_title|default='已删除商品'}} + + {{if $ticket.verify_status == 0}} + 未核销 + {{elseif $ticket.verify_status == 1}} + 已核销 + {{elseif $ticket.verify_status == 2}} + 已退款 + {{/if}} + + {{if !empty($ticket.issued_at)}}{{:date('Y-m-d H:i', $ticket.issued_at)}}{{else}}—{{/if}} + + + + 查看详情 + + + + {{/volist}} {{else}} - - 暂无数据 - + + 暂无数据 + {{/if}} @@ -123,4 +146,4 @@
-{{:ModuleInclude('public/footer')}} +{{:ModuleInclude('public/footer')}} \ No newline at end of file diff --git a/shopxo/app/plugins/vr_ticket/view/admin/ticket/verify.html b/shopxo/app/plugins/vr_ticket/view/admin/ticket/verify.html index ab7f43e..c8b44a4 100644 --- a/shopxo/app/plugins/vr_ticket/view/admin/ticket/verify.html +++ b/shopxo/app/plugins/vr_ticket/view/admin/ticket/verify.html @@ -1,40 +1,50 @@ {{:ModuleInclude('public/header')}} + +
-
- - 电子票列表 - - 票码核销 +
+ 票码核销
-
+
今日核销
-
加载中...
+
-
-
+
待核销
-
加载中...
+
-
-
+
已核销总数
-
加载中...
+
-
-
+
扫码/输入核销
- +
@@ -51,7 +61,7 @@
-
@@ -77,7 +87,8 @@ -
@@ -88,161 +99,123 @@ {{:ModuleInclude('public/footer')}} \ No newline at end of file diff --git a/shopxo/app/plugins/vr_ticket/view/admin/verification/list.html b/shopxo/app/plugins/vr_ticket/view/admin/verification/list.html index d20ff71..ffdecbe 100644 --- a/shopxo/app/plugins/vr_ticket/view/admin/verification/list.html +++ b/shopxo/app/plugins/vr_ticket/view/admin/verification/list.html @@ -1,21 +1,29 @@ {{:ModuleInclude('public/header')}} + +
-
- - 返回 - - 核销记录 +
+ 核销记录
-
-
- +
+
+
-
+
-
+
- + + 重置 +
+
- - - - - - - - - - - - - - - {{if !empty($list)}} - {{foreach $list as $item}} - - - - - - - - - - {{/foreach}} - {{else}} - - - - {{/if}} - -
ID票码核销员商品座位信息观演人核销时间
{{$item.id}}{{$item.ticket_code}}{{$item.verifier_name|default='-'}}{{$item.goods_title|default='-'}}{{$item.seat_info|default='-'}}{{$item.real_name|default='-'}}{{if !empty($item.created_at)}}{{date('Y-m-d H:i', $item.created_at)}}{{/if}}
暂无数据
+ +
+
核销记录 共 {{$count|default=0}} 条
+
+ + + + + + + + + + + + + + {{if !empty($list)}} + {{foreach $list as $item}} + + + + + + + + + + {{/foreach}} + {{else}} + + + + {{/if}} + +
ID票码核销员商品座位信息观演人核销时间
{{$item.id}}{{$item.ticket_code}}{{$item.verifier_name|default='-'}}{{$item.goods_title|default='-'}}{{$item.seat_info|default='-'}}{{$item.real_name|default='-'}}{{if !empty($item.created_at)}}{{$item.created_at|date='Y-m-d H:i'}}{{else}}-{{/if}}
暂无数据
- - {{if !empty($page)}}{{$page|raw}}{{/if}} + + {{if !empty($page)}} +
+ {{$page|raw}} +
+ {{/if}} +
diff --git a/shopxo/app/plugins/vr_ticket/view/admin/verifier/list.html b/shopxo/app/plugins/vr_ticket/view/admin/verifier/list.html index 817b3ab..4d6def7 100644 --- a/shopxo/app/plugins/vr_ticket/view/admin/verifier/list.html +++ b/shopxo/app/plugins/vr_ticket/view/admin/verifier/list.html @@ -1,104 +1,110 @@ {{:ModuleInclude('public/header')}} + +
-
- - 返回 - - 核销员管理 +
+ 核销员管理
-
-
-
+
+
+
-
+
- - - 添加核销员 + + 重置 + + 添加核销员 +
+
- - - - - - - - - - - - - - {{if !empty($list)}} - {{foreach $list as $item}} - - - - - - - - - {{/foreach}} - {{else}} - - - - {{/if}} - -
ID核销员名称关联用户状态添加时间操作
{{$item.id}}{{$item.name}}{{$item.user_name|default='-'}} - {{if $item.status == 1}} - 启用 - {{else}} - 禁用 - {{/if}} - {{if !empty($item.created_at)}}{{date('Y-m-d H:i', $item.created_at)}}{{/if}} - - 编辑 - - -
暂无数据
+ +
+
核销员列表 共 {{$count|default=0}} 条
+
+ + + + + + + + + + + + + {{if !empty($list)}} + {{foreach $list as $item}} + + + + + + + + + {{/foreach}} + {{else}} + + + + {{/if}} + +
ID核销员名称关联用户状态添加时间操作
{{$item.id}}{{$item.name}}{{$item.user_name|default='-'}} + {{if $item.status == 1}} + 启用 + {{else}} + 禁用 + {{/if}} + {{if !empty($item.created_at)}}{{$item.created_at|date='Y-m-d H:i'}}{{else}}-{{/if}} + + 编辑 + + {{if $item.status == 1}} + + {{else}} + + {{/if}} +
暂无数据
- - {{if !empty($page)}}{{$page|raw}}{{/if}} + + {{if !empty($page)}} +
+ {{$page|raw}} +
+ {{/if}} +
- - {{:ModuleInclude('public/footer')}} \ No newline at end of file diff --git a/shopxo/app/plugins/vr_ticket/view/venue/list.html b/shopxo/app/plugins/vr_ticket/view/venue/list.html index e29e1fc..dfad479 100644 --- a/shopxo/app/plugins/vr_ticket/view/venue/list.html +++ b/shopxo/app/plugins/vr_ticket/view/venue/list.html @@ -3,15 +3,6 @@