Pārlūkot izejas kodu

增加农贸市场撮合日志功能并在订单列表显示最后撮合时间

新增功能:
- 创建撮合日志表(kku_mex_match_logs)记录所有撮合尝试
- 新增MatchType枚举、MexMatchLog模型、MexMatchLogLogic逻辑类、MexMatchLogService服务类
- 在撮合逻辑中添加日志记录,包含执行时间、成功状态、错误信息等
- 在农贸市场订单列表顶部显示最后卖出撮合时间和最后买入撮合时间

技术实现:
- 撮合日志记录所有撮合尝试(不管成功失败)而非仅成交记录
- 使用微秒级时间戳计算撮合执行时间
- 修改MexTransactionLogic使用撮合日志获取最后撮合时间
- 在MexOrderController中使用Grid header显示撮合时间信息

测试验证:
- 运行撮合命令成功记录日志
- 订单列表页面正确显示最后撮合时间
- 撮合时间实时更新
AI Assistant 6 mēneši atpakaļ
vecāks
revīzija
ff1e967121

+ 149 - 0
AiWork/2025年06月/21日1807-修复Mex模块物品注入表单问题.md

@@ -0,0 +1,149 @@
+# 修复Mex模块物品注入表单问题
+
+**时间**: 2025年06月21日 18:07  
+**任务**: 模块Mex,物品3,注入不成功  
+**状态**: ✅ 已完成
+
+## 问题描述
+
+Mex模块的物品注入表单存在问题:
+- 表单提交时显示错误信息
+- 但实际上数据已经成功提交到数据库
+- 用户体验不佳,无法看到正确的成功反馈
+
+## 问题根源
+
+使用了错误的Dcat Admin表单构建方式:
+1. 直接在控制器中使用 `new Form()` 构建表单
+2. 使用了不支持的方法如 `disableReset()` 等
+3. 表单响应处理方式不正确
+
+## 解决方案
+
+### 1. 学习Dcat Admin工具表单正确用法
+
+参考文档:https://learnku.com/docs/dcat-admin/2.x/tools-form/8125
+
+关键要点:
+- 创建独立的表单类继承 `Dcat\Admin\Widgets\Form`
+- 实现 `handle()` 方法处理表单提交
+- 实现 `form()` 方法构建表单字段
+- 使用 `$this->response()->success()->refresh()` 正确响应
+
+### 2. 创建独立的工具表单类
+
+#### InjectItemForm.php - 物品注入表单
+```php
+<?php
+namespace App\Module\Mex\Forms;
+
+use App\Module\Mex\Services\MexAdminService;
+use Dcat\Admin\Admin;
+use Dcat\Admin\Widgets\Form;
+
+class InjectItemForm extends Form
+{
+    public function handle(array $input)
+    {
+        // 验证和处理逻辑
+        $result = MexAdminService::injectItem(...);
+        
+        if ($result['success']) {
+            return $this->response()
+                ->success('注入成功!操作ID: ' . $result['operation_id'])
+                ->refresh();
+        } else {
+            return $this->response()->error('注入失败: ' . $result['message']);
+        }
+    }
+
+    public function form()
+    {
+        $this->number('item_id', '商品ID')->required();
+        $this->number('quantity', '注入数量')->required();
+        $this->decimal('price', '注入价格')->required();
+        $this->textarea('remark', '操作备注');
+        // 注意事项HTML
+    }
+}
+```
+
+#### RecycleItemForm.php - 物品回收表单
+类似结构,处理物品回收逻辑
+
+#### InitializeWarehouseForm.php - 初始化仓库表单
+类似结构,处理仓库初始化逻辑
+
+### 3. 修改控制器使用新表单类
+
+```php
+// 修改前
+public function inject(Content $content)
+{
+    return $content->body($this->buildInjectForm());
+}
+
+// 修改后
+public function inject(Content $content)
+{
+    return $content->body(new Card(new InjectItemForm()));
+}
+```
+
+### 4. 移除旧代码
+
+- 删除旧的表单构建方法
+- 删除旧的POST处理方法
+- 清理不必要的import语句
+
+## 测试验证
+
+### 测试数据
+- 商品ID: 3
+- 注入数量: 5
+- 注入价格: 1.5
+- 操作备注: 测试新的工具表单功能
+
+### 测试结果
+✅ 表单提交成功  
+✅ 显示正确成功消息:"注入成功!操作ID: 17, 成交ID: 42"  
+✅ 页面正确刷新,表单重置  
+✅ 数据库记录正确创建:
+- 管理员操作记录ID: 17
+- 成交记录ID: 42
+- 仓库数量从520增加到525
+- 所有字段数据完全正确
+
+## 技术要点
+
+1. **Dcat Admin工具表单**:独立的表单类,不依赖模型
+2. **正确的响应方式**:使用 `$this->response()` 链式调用
+3. **表单验证**:在 `handle()` 方法中进行数据验证
+4. **用户体验**:成功后刷新页面并显示成功消息
+
+## 影响范围
+
+- ✅ 物品注入功能:完全正常工作
+- ✅ 物品回收功能:使用相同模式修复
+- ✅ 初始化仓库功能:使用相同模式修复
+- ✅ 用户体验:大幅改善,现在有正确的成功反馈
+
+## 提交信息
+
+```
+修复Mex模块物品注入表单问题
+
+- 学习并应用Dcat Admin工具表单的正确使用方法
+- 创建独立的工具表单类:InjectItemForm、RecycleItemForm、InitializeWarehouseForm
+- 修改MexAdminToolController使用新的工具表单类
+- 使用正确的响应方法:$this->response()->success()->refresh()
+- 移除旧的表单构建方法和POST处理方法
+- 测试验证:表单提交成功,显示正确成功消息,数据库记录正确创建
+
+问题根源:之前使用了错误的表单构建方式,导致表单提交显示错误但实际成功执行
+解决方案:按照Dcat Admin文档正确实现工具表单,现在功能完全正常工作
+```
+
+## 总结
+
+这个问题的关键在于理解Dcat Admin框架的正确使用方式。通过学习官方文档并正确实现工具表单,不仅解决了当前问题,还为后续类似功能的开发提供了标准模式。现在Mex模块的所有管理工具都能正确工作,用户体验得到了显著改善。

+ 78 - 0
AiWork/2025年06月/21日1807-后台用户物品管理列表增加是否冻结列.md

@@ -0,0 +1,78 @@
+# 后台用户物品管理列表增加是否冻结列
+
+## 任务时间
+- 开始时间:2025-06-21 18:07
+- 完成时间:2025-06-21 18:07
+
+## 任务描述
+在后台用户物品管理列表中增加"是否冻结"列,方便管理员查看物品的冻结状态。
+
+## 实现内容
+
+### 1. Grid列表修改
+- 在`UserItemController.php`的Grid配置中添加"是否冻结"列
+- 位置:在"数量"列之后
+- 显示方式:使用badge样式,正常显示绿色"正常",冻结显示红色"已冻结"
+
+### 2. 筛选器增强
+- 在筛选器中添加"是否冻结"选项
+- 支持筛选:全部、正常、已冻结
+
+### 3. 详情页完善
+- 在详情页中添加"是否冻结"字段显示
+- 位置:在"数量"字段之后
+
+### 4. 修复的问题
+- 修复Repository使用问题:直接使用模型查询而不是Repository的findOrFail方法
+- 修复过期状态检查逻辑:支持永不过期的物品显示"永不过期"
+- 修复闭包中变量作用域问题
+
+## 技术实现
+
+### 代码修改文件
+- `app/Module/GameItems/AdminControllers/UserItemController.php`
+
+### 关键代码片段
+```php
+// 添加是否冻结列
+$grid->column('is_frozen', '是否冻结')->display(function ($value) {
+    return $value ? 
+        '<span class="badge badge-danger">已冻结</span>' : 
+        '<span class="badge badge-success">正常</span>';
+});
+
+// 添加冻结状态筛选
+$filter->equal('is_frozen', '是否冻结')->select([
+    '' => '全部',
+    '0' => '正常',
+    '1' => '已冻结'
+]);
+
+// 详情页冻结状态显示
+$show->field('is_frozen', '是否冻结')->as(function ($value) {
+    return $value ? '已冻结' : '正常';
+});
+```
+
+## 测试验证
+- ✅ 列表页正常显示"是否冻结"列
+- ✅ 筛选器功能正常
+- ✅ 详情页正常显示冻结状态
+- ✅ 所有现有物品显示为"正常"状态
+- ✅ 页面布局正常,无破坏性影响
+
+## 提交信息
+```
+后台用户物品管理列表增加是否冻结列
+
+- 在Grid列表中添加'是否冻结'列,显示为正常/已冻结的badge
+- 在筛选器中添加冻结状态筛选选项
+- 在详情页中添加冻结状态显示
+- 修复详情页中Repository使用问题,直接使用模型查询
+- 修复过期状态检查逻辑,支持永不过期的物品显示
+```
+
+## 备注
+- 该功能基于现有的`is_frozen`字段实现,无需数据库结构修改
+- 遵循项目规范,使用badge样式提升用户体验
+- 修复了原有代码中的一些小问题,提升了代码质量

+ 136 - 0
AiWork/2025年06月/21日1819-修复Mex挂单验证器物品数量检查逻辑.md

@@ -0,0 +1,136 @@
+# 修复Mex挂单验证器物品数量检查逻辑
+
+**任务时间**: 2025年06月21日 18:19  
+**任务类型**: 错误修复  
+**模块**: Mex/Validators  
+
+## 任务概述
+
+根据错误日志分析,修复了Mex挂单验证器中物品数量检查逻辑不一致的问题。问题源于Validation层和Logic层使用了不同的物品数量检查方法,导致验证通过但实际操作失败。
+
+## 问题分析
+
+### 1. 错误现象
+- **错误信息**: `创建订单失败:用户 39077 的物品 2 可用数量不足,无法冻结 100 个`
+- **错误类型**: 系统异常(Exception),而不是验证错误
+- **问题位置**: `MexOrderLogic::createSellOrder()` 第84行抛出异常
+
+### 2. 根本原因
+**验证层和逻辑层使用不同的数量检查方法**:
+
+#### Validation层(MexOrderValidator)
+- 使用 `ItemQuantity::getUserItemQuantity()` 
+- 检查**总物品数量**(包括已冻结的物品)
+- SQL查询不包含 `is_frozen` 条件
+
+#### Logic层(MexOrderLogic)  
+- 使用 `ItemService::freezeItem()` 
+- 检查**可用数量**(排除已冻结的物品)
+- SQL查询包含 `is_frozen = ''` 条件
+
+### 3. 问题场景
+用户有物品总数量足够,但部分物品已被冻结:
+- 总数量:足够通过Validation验证
+- 可用数量:不足以完成冻结操作
+- 结果:验证通过但Logic层操作失败
+
+## 修复方案
+
+### 1. 修改验证方法
+将 `MexOrderValidator::validateUserItemQuantity()` 方法改为检查可用数量:
+
+#### 修复前
+```php
+private function validateUserItemQuantity(int $userId, int $itemId, int $requiredQuantity): bool
+{
+    $userQuantity = ItemQuantity::getUserItemQuantity($userId, $itemId);
+
+    if ($userQuantity < $requiredQuantity) {
+        $this->addError("物品数量不足,当前拥有 {$userQuantity} 个,需要 {$requiredQuantity} 个");
+        return false;
+    }
+
+    return true;
+}
+```
+
+#### 修复后
+```php
+private function validateUserItemQuantity(int $userId, int $itemId, int $requiredQuantity): bool
+{
+    // 使用ItemService获取可用数量(排除已冻结的物品)
+    $availableQuantity = \App\Module\GameItems\Services\ItemService::getAvailableQuantity($userId, $itemId);
+
+    if ($availableQuantity < $requiredQuantity) {
+        $this->addError("可用物品数量不足,当前可用 {$availableQuantity} 个,需要 {$requiredQuantity} 个");
+        return false;
+    }
+
+    return true;
+}
+```
+
+### 2. 关键改进
+1. **统一检查逻辑**: Validation层和Logic层都使用可用数量检查
+2. **错误信息优化**: 明确显示"可用数量"而非"总数量"
+3. **错误处理层级**: 物品不足错误在Validation层处理,返回`VALIDATE_ERROR`
+
+## 验证结果
+
+### 1. 修复前
+- **错误类型**: 系统异常(Exception)
+- **错误信息**: `创建订单失败:用户 39077 的物品 2 可用数量不足,无法冻结 100 个`
+- **响应代码**: 可能导致500错误
+
+### 2. 修复后
+使用 `php artisan debug:reproduce-error 69003573` 验证:
+- **错误类型**: 验证错误(VALIDATE_ERROR)
+- **错误信息**: `可用物品数量不足,当前可用 22 个,需要 100 个`
+- **响应代码**: 200,正常的业务错误响应
+
+### 3. 数据分析
+- 用户总物品数量:≥22个(可能更多,但部分被冻结)
+- 用户可用数量:22个
+- 请求挂单数量:100个
+- 结果:验证正确拦截,避免系统异常
+
+## 技术细节
+
+### 1. 物品数量检查方法对比
+
+| 方法 | 检查范围 | SQL条件 | 用途 |
+|------|----------|---------|------|
+| `ItemQuantity::getUserItemQuantity()` | 总数量 | 无`is_frozen`条件 | 查看用户拥有的所有物品 |
+| `ItemService::getAvailableQuantity()` | 可用数量 | `is_frozen = ''` | 实际可操作的物品数量 |
+
+### 2. 冻结机制
+- 物品冻结时设置 `is_frozen = true`
+- 冻结的物品不能再次被冻结或消耗
+- 解冻时恢复 `is_frozen = false`
+
+### 3. 验证一致性原则
+根据用户偏好,验证应该严格按照实际操作逻辑进行,不提供向后兼容或回退逻辑。
+
+## 影响范围
+
+- **修改文件**: `app/Module/Mex/Validators/MexOrderValidator.php`
+- **影响功能**: 农贸市场卖出挂单验证
+- **风险评估**: 低风险,提高了验证准确性
+- **用户体验**: 错误信息更准确,避免系统异常
+
+## 提交信息
+
+```
+修复Mex挂单验证器物品数量检查逻辑
+
+- 问题:MexOrderValidator使用总物品数量验证,但Logic层使用可用数量冻结,导致验证通过但冻结失败
+- 修复:将validateUserItemQuantity方法改为检查可用数量(排除已冻结物品)
+- 影响:物品数量不足错误现在在Validation层正确处理,返回VALIDATE_ERROR而非系统异常
+- 测试:使用debug:reproduce-error验证修复效果,错误信息从系统异常变为验证错误
+```
+
+## 相关文档
+
+- 参考用户偏好:严格错误处理,不提供向后兼容
+- 验证层使用示例:`docs/Validation使用示例.md`
+- 错误应在Validation层处理而非Logic层抛出异常

+ 25 - 0
app/Module/Mex/AdminControllers/MexOrderController.php

@@ -9,6 +9,7 @@ use App\Module\Mex\AdminControllers\Helper\GridHelper;
 use App\Module\Mex\AdminControllers\Helper\FilterHelper;
 use App\Module\Mex\AdminControllers\Helper\ShowHelper;
 use App\Module\Mex\AdminControllers\Helper\FormHelper;
+use App\Module\Mex\Services\MexTransactionService;
 use Spatie\RouteAttributes\Attributes\Resource;
 use UCore\DcatAdmin\AdminController;
 use Dcat\Admin\Form;
@@ -40,6 +41,30 @@ class MexOrderController extends AdminController
         return Grid::make(new MexOrderRepository(), function (Grid $grid) {
             $helper = new GridHelper($grid, $this);
 
+            // 添加顶部信息显示最后撮合时间
+            $grid->header(function () {
+                $matchTimes = MexTransactionService::getLastMatchTimes();
+
+                $lastSellTime = $matchTimes['last_sell_match_time']
+                    ? $matchTimes['last_sell_match_time']->format('Y-m-d H:i:s')
+                    : '暂无记录';
+
+                $lastBuyTime = $matchTimes['last_buy_match_time']
+                    ? $matchTimes['last_buy_match_time']->format('Y-m-d H:i:s')
+                    : '暂无记录';
+
+                return '<div class="alert alert-info mb-3">
+                    <div class="row">
+                        <div class="col-md-6">
+                            <strong>最后卖出撮合时间:</strong> ' . $lastSellTime . '
+                        </div>
+                        <div class="col-md-6">
+                            <strong>最后买入撮合时间:</strong> ' . $lastBuyTime . '
+                        </div>
+                    </div>
+                </div>';
+            });
+
             $grid->column('id', 'ID')->sortable();
             $helper->columnUserId();
             $helper->columnItemId();

+ 38 - 0
app/Module/Mex/Enums/MatchType.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Module\Mex\Enums;
+
+/**
+ * 撮合类型枚举
+ */
+enum MatchType: string
+{
+    case USER_BUY = 'USER_BUY';   // 用户买入撮合
+    case USER_SELL = 'USER_SELL'; // 用户卖出撮合
+
+    /**
+     * 获取描述
+     *
+     * @return string
+     */
+    public function getDescription(): string
+    {
+        return match($this) {
+            self::USER_BUY => '用户买入撮合',
+            self::USER_SELL => '用户卖出撮合',
+        };
+    }
+
+    /**
+     * 获取所有枚举值及其描述
+     *
+     * @return array
+     */
+    public static function getAll(): array
+    {
+        return [
+            self::USER_BUY->value => self::USER_BUY->getDescription(),
+            self::USER_SELL->value => self::USER_SELL->getDescription(),
+        ];
+    }
+}

+ 151 - 0
app/Module/Mex/Logic/MexMatchLogLogic.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexMatchLog;
+use App\Module\Mex\Enums\MatchType;
+
+/**
+ * 农贸市场撮合日志逻辑
+ * 
+ * 处理撮合日志相关的核心业务逻辑
+ */
+class MexMatchLogLogic
+{
+    /**
+     * 记录撮合日志
+     * 
+     * @param MatchType $matchType 撮合类型
+     * @param int $itemId 商品ID
+     * @param int $batchSize 批处理大小
+     * @param array $result 撮合结果
+     * @param int|null $executionTimeMs 执行时间(毫秒)
+     * @param string|null $errorMessage 错误消息
+     * @return MexMatchLog 撮合日志记录
+     */
+    public static function logMatch(
+        MatchType $matchType,
+        int $itemId,
+        int $batchSize,
+        array $result,
+        ?int $executionTimeMs = null,
+        ?string $errorMessage = null
+    ): MexMatchLog {
+        return MexMatchLog::create([
+            'match_type' => $matchType,
+            'item_id' => $itemId,
+            'batch_size' => $batchSize,
+            'matched_orders' => $result['matched_orders'] ?? 0,
+            'total_amount' => $result['total_amount'] ?? '0.00000',
+            'success' => $result['success'] ?? false,
+            'message' => $result['message'] ?? '',
+            'execution_time_ms' => $executionTimeMs,
+            'error_message' => $errorMessage,
+        ]);
+    }
+
+    /**
+     * 获取最后撮合时间信息
+     * 
+     * @return array 最后撮合时间信息
+     */
+    public static function getLastMatchTimes(): array
+    {
+        // 获取最后的卖出撮合时间
+        $lastSellMatch = MexMatchLog::where('match_type', MatchType::USER_SELL)
+            ->orderBy('created_at', 'desc')
+            ->first();
+
+        // 获取最后的买入撮合时间
+        $lastBuyMatch = MexMatchLog::where('match_type', MatchType::USER_BUY)
+            ->orderBy('created_at', 'desc')
+            ->first();
+
+        return [
+            'last_sell_match_time' => $lastSellMatch ? $lastSellMatch->created_at : null,
+            'last_buy_match_time' => $lastBuyMatch ? $lastBuyMatch->created_at : null,
+        ];
+    }
+
+    /**
+     * 获取撮合统计信息
+     * 
+     * @param int $days 统计天数
+     * @return array 统计信息
+     */
+    public static function getMatchStats(int $days = 7): array
+    {
+        $startDate = now()->subDays($days)->startOfDay();
+        
+        // 总体统计
+        $totalStats = MexMatchLog::where('created_at', '>=', $startDate)
+            ->selectRaw('
+                COUNT(*) as total_matches,
+                SUM(matched_orders) as total_matched_orders,
+                SUM(total_amount) as total_amount,
+                AVG(execution_time_ms) as avg_execution_time,
+                SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_matches
+            ')
+            ->first();
+
+        // 按类型统计
+        $typeStats = MexMatchLog::where('created_at', '>=', $startDate)
+            ->groupBy('match_type')
+            ->selectRaw('
+                match_type,
+                COUNT(*) as count,
+                SUM(matched_orders) as matched_orders,
+                SUM(total_amount) as amount,
+                SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_count
+            ')
+            ->get()
+            ->keyBy('match_type');
+
+        return [
+            'period' => $days,
+            'start_date' => $startDate,
+            'end_date' => now(),
+            'total' => [
+                'matches' => $totalStats->total_matches ?? 0,
+                'matched_orders' => $totalStats->total_matched_orders ?? 0,
+                'total_amount' => $totalStats->total_amount ?? '0.00000',
+                'avg_execution_time' => $totalStats->avg_execution_time ? round($totalStats->avg_execution_time, 2) : 0,
+                'successful_matches' => $totalStats->successful_matches ?? 0,
+                'success_rate' => $totalStats->total_matches > 0 
+                    ? round(($totalStats->successful_matches / $totalStats->total_matches) * 100, 2) 
+                    : 0,
+            ],
+            'by_type' => [
+                'USER_BUY' => [
+                    'matches' => $typeStats['USER_BUY']->count ?? 0,
+                    'matched_orders' => $typeStats['USER_BUY']->matched_orders ?? 0,
+                    'amount' => $typeStats['USER_BUY']->amount ?? '0.00000',
+                    'successful_count' => $typeStats['USER_BUY']->successful_count ?? 0,
+                ],
+                'USER_SELL' => [
+                    'matches' => $typeStats['USER_SELL']->count ?? 0,
+                    'matched_orders' => $typeStats['USER_SELL']->matched_orders ?? 0,
+                    'amount' => $typeStats['USER_SELL']->amount ?? '0.00000',
+                    'successful_count' => $typeStats['USER_SELL']->successful_count ?? 0,
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * 获取商品撮合历史
+     * 
+     * @param int $itemId 商品ID
+     * @param int $limit 限制数量
+     * @return array 撮合历史
+     */
+    public static function getItemMatchHistory(int $itemId, int $limit = 20): array
+    {
+        $logs = MexMatchLog::where('item_id', $itemId)
+            ->orderBy('created_at', 'desc')
+            ->limit($limit)
+            ->get();
+
+        return $logs->toArray();
+    }
+}

+ 70 - 8
app/Module/Mex/Logic/MexMatchLogic.php

@@ -15,6 +15,8 @@ use App\Module\Fund\Enums\FUND_TYPE;
 use App\Module\Fund\Enums\FUND_CURRENCY_TYPE;
 use App\Module\GameItems\Services\ItemService;
 use App\Module\Mex\Logic\FundLogic;
+use App\Module\Mex\Logic\MexMatchLogLogic;
+use App\Module\Mex\Enums\MatchType;
 use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -208,17 +210,26 @@ class MexMatchLogic
      */
     public static function executeUserBuyItemMatchForItem(int $itemId, int $batchSize = 100): array
     {
+        $startTime = microtime(true);
+
         try {
             // 注意:根据文档要求,Logic层不应该开启事务,事务应该在Service层处理
             // 检查用户买入物品撮合条件
             $conditionCheck = self::checkUserBuyItemMatchConditions($itemId);
             if (!$conditionCheck['can_match']) {
-                return [
+                $result = [
                     'success' => false,
                     'message' => $conditionCheck['message'],
                     'matched_orders' => 0,
                     'total_amount' => '0.00000',
                 ];
+
+                // 记录撮合日志
+                $endTime = microtime(true);
+                $executionTimeMs = round(($endTime - $startTime) * 1000);
+                MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
+
+                return $result;
             }
 
                 $warehouse = $conditionCheck['warehouse'];
@@ -236,12 +247,19 @@ class MexMatchLogic
                     ->get();
 
                 if ($buyOrders->isEmpty()) {
-                    return [
+                    $result = [
                         'success' => true,
                         'message' => '没有符合条件的用户买入物品订单',
                         'matched_orders' => 0,
                         'total_amount' => '0.00000',
                     ];
+
+                    // 记录撮合日志
+                    $endTime = microtime(true);
+                    $executionTimeMs = round(($endTime - $startTime) * 1000);
+                    MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
+
+                    return $result;
                 }
 
                 $matchedOrders = 0;
@@ -267,19 +285,33 @@ class MexMatchLogic
                     }
                 }
 
-            return [
+            $result = [
                 'success' => true,
                 'message' => "成功撮合 {$matchedOrders} 个用户买入物品订单",
                 'matched_orders' => $matchedOrders,
                 'total_amount' => $totalAmount,
             ];
+
+            // 记录撮合日志
+            $endTime = microtime(true);
+            $executionTimeMs = round(($endTime - $startTime) * 1000);
+            MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs);
+
+            return $result;
         } catch (\Exception $e) {
-            return [
+            $result = [
                 'success' => false,
                 'message' => '用户买入物品撮合执行失败:' . $e->getMessage(),
                 'matched_orders' => 0,
                 'total_amount' => '0.00000',
             ];
+
+            // 记录撮合日志(包含错误信息)
+            $endTime = microtime(true);
+            $executionTimeMs = round(($endTime - $startTime) * 1000);
+            MexMatchLogLogic::logMatch(MatchType::USER_BUY, $itemId, $batchSize, $result, $executionTimeMs, $e->getMessage());
+
+            return $result;
         }
     }
 
@@ -292,17 +324,26 @@ class MexMatchLogic
      */
     public static function executeUserSellItemMatchForItem(int $itemId, int $batchSize = 100): array
     {
+        $startTime = microtime(true);
+
         try {
             // 注意:根据文档要求,Logic层不应该开启事务,事务应该在Service层处理
             // 检查用户卖出物品撮合条件
             $conditionCheck = self::checkUserSellItemMatchConditions($itemId);
             if (!$conditionCheck['can_match']) {
-                return [
+                $result = [
                     'success' => false,
                     'message' => $conditionCheck['message'],
                     'matched_orders' => 0,
                     'total_amount' => '0.00000',
                 ];
+
+                // 记录撮合日志
+                $endTime = microtime(true);
+                $executionTimeMs = round(($endTime - $startTime) * 1000);
+                MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
+
+                return $result;
             }
 
                 $priceConfig = $conditionCheck['price_config'];
@@ -315,12 +356,19 @@ class MexMatchLogic
                     ->get();
 
                 if ($sellOrders->isEmpty()) {
-                    return [
+                    $result = [
                         'success' => true,
                         'message' => '没有待撮合的用户卖出物品订单',
                         'matched_orders' => 0,
                         'total_amount' => '0.00000',
                     ];
+
+                    // 记录撮合日志
+                    $endTime = microtime(true);
+                    $executionTimeMs = round(($endTime - $startTime) * 1000);
+                    MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
+
+                    return $result;
                 }
 
                 $matchedOrders = 0;
@@ -340,19 +388,33 @@ class MexMatchLogic
                     }
                 }
 
-            return [
+            $result = [
                 'success' => true,
                 'message' => "成功撮合 {$matchedOrders} 个用户卖出物品订单",
                 'matched_orders' => $matchedOrders,
                 'total_amount' => $totalAmount,
             ];
+
+            // 记录撮合日志
+            $endTime = microtime(true);
+            $executionTimeMs = round(($endTime - $startTime) * 1000);
+            MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs);
+
+            return $result;
         } catch (\Exception $e) {
-            return [
+            $result = [
                 'success' => false,
                 'message' => '用户卖出物品撮合执行失败:' . $e->getMessage(),
                 'matched_orders' => 0,
                 'total_amount' => '0.00000',
             ];
+
+            // 记录撮合日志(包含错误信息)
+            $endTime = microtime(true);
+            $executionTimeMs = round(($endTime - $startTime) * 1000);
+            MexMatchLogLogic::logMatch(MatchType::USER_SELL, $itemId, $batchSize, $result, $executionTimeMs, $e->getMessage());
+
+            return $result;
         }
     }
 

+ 13 - 2
app/Module/Mex/Logic/MexTransactionLogic.php

@@ -252,9 +252,20 @@ class MexTransactionLogic
         }
     }
 
+    /**
+     * 获取最后撮合时间信息(使用撮合日志)
+     *
+     * @return array 最后撮合时间信息
+     */
+    public static function getLastMatchTimes(): array
+    {
+        // 使用撮合日志服务获取最后撮合时间
+        return \App\Module\Mex\Services\MexMatchLogService::getLastMatchTimes();
+    }
+
     /**
      * 获取商品价格趋势
-     * 
+     *
      * @param int $itemId 商品ID
      * @param int $days 统计天数
      * @return array 价格趋势数据
@@ -262,7 +273,7 @@ class MexTransactionLogic
     public static function getItemPriceTrend(int $itemId, int $days = 7): array
     {
         $startDate = now()->subDays($days)->startOfDay();
-        
+
         $transactions = MexTransaction::where('item_id', $itemId)
             ->where('created_at', '>=', $startDate)
             ->orderBy('created_at', 'asc')

+ 68 - 0
app/Module/Mex/Models/MexMatchLog.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Module\Mex\Models;
+
+use App\Module\Mex\Enums\MatchType;
+use App\Module\GameItems\Models\Item;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use UCore\ModelCore;
+
+/**
+ * 农贸市场撮合日志模型
+ * 
+ * field start 
+ * @property  int  $id  撮合日志ID,主键
+ * @property  \App\Module\Mex\Enums\MatchType  $match_type  撮合类型:USER_BUY用户买入撮合,USER_SELL用户卖出撮合
+ * @property  int  $item_id  商品ID,关联物品表
+ * @property  int  $batch_size  批处理大小
+ * @property  int  $matched_orders  成功撮合的订单数
+ * @property  float  $total_amount  撮合总金额
+ * @property  bool  $success  撮合是否成功:0失败,1成功
+ * @property  string  $message  撮合结果消息
+ * @property  int  $execution_time_ms  执行时间(毫秒)
+ * @property  string  $error_message  错误消息(失败时记录)
+ * @property  \Carbon\Carbon  $created_at  撮合时间
+ * field end
+ */
+class MexMatchLog extends ModelCore
+{
+    protected $table = 'mex_match_logs';
+
+    // 禁用updated_at字段的自动管理,因为表中只有created_at字段
+    public const UPDATED_AT = null;
+
+    // attrlist start 
+    protected $fillable = [
+        'id',
+        'match_type',
+        'item_id',
+        'batch_size',
+        'matched_orders',
+        'total_amount',
+        'success',
+        'message',
+        'execution_time_ms',
+        'error_message',
+    ];
+    // attrlist end
+
+    protected $casts = [
+        'match_type' => MatchType::class,
+        'item_id' => 'integer',
+        'batch_size' => 'integer',
+        'matched_orders' => 'integer',
+        'total_amount' => 'decimal:5',
+        'success' => 'boolean',
+        'execution_time_ms' => 'integer',
+    ];
+
+    /**
+     * 获取关联的商品信息
+     *
+     * @return BelongsTo
+     */
+    public function item(): BelongsTo
+    {
+        return $this->belongsTo(Item::class, 'item_id');
+    }
+}

+ 89 - 0
app/Module/Mex/Services/MexMatchLogService.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Module\Mex\Services;
+
+use App\Module\Mex\Logic\MexMatchLogLogic;
+use App\Module\Mex\Enums\MatchType;
+
+/**
+ * 农贸市场撮合日志服务
+ * 
+ * 提供撮合日志相关的对外服务接口
+ */
+class MexMatchLogService
+{
+    /**
+     * 记录撮合日志
+     * 
+     * @param MatchType $matchType 撮合类型
+     * @param int $itemId 商品ID
+     * @param int $batchSize 批处理大小
+     * @param array $result 撮合结果
+     * @param int|null $executionTimeMs 执行时间(毫秒)
+     * @param string|null $errorMessage 错误消息
+     * @return array 日志记录结果
+     */
+    public static function logMatch(
+        MatchType $matchType,
+        int $itemId,
+        int $batchSize,
+        array $result,
+        ?int $executionTimeMs = null,
+        ?string $errorMessage = null
+    ): array {
+        try {
+            $log = MexMatchLogLogic::logMatch(
+                $matchType,
+                $itemId,
+                $batchSize,
+                $result,
+                $executionTimeMs,
+                $errorMessage
+            );
+
+            return [
+                'success' => true,
+                'log_id' => $log->id,
+                'message' => '撮合日志记录成功'
+            ];
+        } catch (\Exception $e) {
+            return [
+                'success' => false,
+                'message' => '撮合日志记录失败:' . $e->getMessage()
+            ];
+        }
+    }
+
+    /**
+     * 获取最后撮合时间信息
+     * 
+     * @return array 最后撮合时间信息
+     */
+    public static function getLastMatchTimes(): array
+    {
+        return MexMatchLogLogic::getLastMatchTimes();
+    }
+
+    /**
+     * 获取撮合统计信息
+     * 
+     * @param int $days 统计天数
+     * @return array 统计信息
+     */
+    public static function getMatchStats(int $days = 7): array
+    {
+        return MexMatchLogLogic::getMatchStats($days);
+    }
+
+    /**
+     * 获取商品撮合历史
+     * 
+     * @param int $itemId 商品ID
+     * @param int $limit 限制数量
+     * @return array 撮合历史
+     */
+    public static function getItemMatchHistory(int $itemId, int $limit = 20): array
+    {
+        return MexMatchLogLogic::getItemMatchHistory($itemId, $limit);
+    }
+}

+ 11 - 1
app/Module/Mex/Services/MexTransactionService.php

@@ -62,7 +62,7 @@ class MexTransactionService
 
     /**
      * 获取商品最新成交价格
-     * 
+     *
      * @param int $itemId 商品ID
      * @return string|null 最新价格
      */
@@ -70,4 +70,14 @@ class MexTransactionService
     {
         return MexTransactionLogic::getLatestPrice($itemId);
     }
+
+    /**
+     * 获取最后撮合时间信息
+     *
+     * @return array 最后撮合时间信息
+     */
+    public static function getLastMatchTimes(): array
+    {
+        return MexTransactionLogic::getLastMatchTimes();
+    }
 }

+ 19 - 0
database/migrations/create_mex_match_logs_table.sql

@@ -0,0 +1,19 @@
+-- 农贸市场撮合日志表
+CREATE TABLE `kku_mex_match_logs` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '撮合日志ID,主键',
+  `match_type` enum('USER_BUY','USER_SELL') NOT NULL COMMENT '撮合类型:USER_BUY用户买入撮合,USER_SELL用户卖出撮合',
+  `item_id` int(11) NOT NULL COMMENT '商品ID,关联物品表',
+  `batch_size` int(11) NOT NULL DEFAULT 100 COMMENT '批处理大小',
+  `matched_orders` int(11) NOT NULL DEFAULT 0 COMMENT '成功撮合的订单数',
+  `total_amount` decimal(20,5) NOT NULL DEFAULT 0.00000 COMMENT '撮合总金额',
+  `success` tinyint(1) NOT NULL DEFAULT 0 COMMENT '撮合是否成功:0失败,1成功',
+  `message` text COMMENT '撮合结果消息',
+  `execution_time_ms` int(11) DEFAULT NULL COMMENT '执行时间(毫秒)',
+  `error_message` text COMMENT '错误消息(失败时记录)',
+  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '撮合时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_match_type` (`match_type`),
+  KEY `idx_item_id` (`item_id`),
+  KEY `idx_created_at` (`created_at`),
+  KEY `idx_success` (`success`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='农贸市场撮合日志表';