Răsfoiți Sursa

完善Mex模块Logic层和创建计划任务

## 完成内容

### 1. 完善Logic层业务逻辑(5个Logic类)
- MexWarehouseLogic:仓库逻辑,库存管理、统计分析、库存操作
- MexTransactionLogic:成交记录逻辑,交易记录查询、统计分析、价格趋势
- MexPriceConfigLogic:价格配置逻辑,价格验证、配置管理、缓存机制
- MexAdminLogic:管理员操作逻辑,物品注入/回收、账户流转、操作记录
- MexMatchLogic:撮合逻辑,三级排序撮合、整单匹配、条件检查

### 2. 创建计划任务(1个Command类)
- MexMatchCommand:撮合计划任务,支持指定商品、批处理、试运行模式

## 核心功能实现

### 撮合算法核心特性
- 三级排序:价格优先(高价优先)→ 时间优先(早下单优先)→ 数量优先(小单优先)
- 整单匹配:订单不拆分,库存充足则全部成交,不足则等待
- 数量保护:过滤超过保护阈值的大额订单,防止市场操控
- 批量处理:支持批处理大小控制,避免内存溢出

### 账户体系集成
- 仓库账户(USER_ID: 15):作为所有交易的中介方
- 调控账户(USER_ID: 16):管理员操作的专用账户
- 完整的资金和物品流转设计(待实现具体账户操作)

### 缓存和性能优化
- 价格配置10分钟缓存,减少数据库查询
- 批量处理机制,支持大量订单高效处理
- 数据库事务保证操作原子性

### 监控和日志
- 详细的操作日志记录
- 完整的统计信息提供
- 异常处理和错误追踪

## 技术特点
- 严格的事务控制确保数据一致性
- 完整的错误处理和异常恢复
- 丰富的统计分析功能
- 支持试运行模式的计划任务
- 5位小数精度的价格计算

Logic层总计1400+行代码,实现了完整的业务逻辑处理
notfff 7 luni în urmă
părinte
comite
b816f8c6b1

+ 266 - 0
AiWork/2025年06月/11日2121-移除Model业务逻辑并创建Repository和Service层.md

@@ -0,0 +1,266 @@
+# 移除Model业务逻辑并创建Repository和Service层
+
+## 任务概述
+- **任务时间**:2025年06月11日 21:21
+- **任务类型**:架构重构和开发
+- **涉及模块**:Mex(农贸市场交易系统)
+- **主要内容**:移除Model中的业务逻辑,创建Repository和Service层
+
+## 任务背景
+用户指出模型中不应存在逻辑,需要进行移除。同时要求继续开发,创建数据仓库,参考Fund模块,数据仓库是后台专用的,其内不存在方法定义。
+
+## 执行过程
+
+### 1. 移除Model中的业务逻辑
+
+#### 1.1 MexOrder模型清理
+- **移除内容**:47行业务逻辑方法
+- **移除方法**:
+  - getRemainingQuantityAttribute():剩余数量计算
+  - getRemainingAmountAttribute():剩余金额计算
+  - isBuyOrder()、isSellOrder():订单类型判断
+  - canMatch():撮合条件判断
+  - isCompleted()、isCancelled()、isFailed():状态判断
+- **保留内容**:字段映射、类型转换、基础属性
+
+#### 1.2 MexWarehouse模型清理
+- **移除内容**:62行业务逻辑方法
+- **移除方法**:
+  - getAverageBuyPriceAttribute():平均买入价格计算
+  - getAverageSellPriceAttribute():平均卖出价格计算
+  - getNetBuyQuantityAttribute():净买入数量计算
+  - hasStock()、hasEnoughStock():库存判断
+  - addStock()、reduceStock():库存操作
+- **保留内容**:数据字段和类型转换
+
+#### 1.3 MexTransaction模型清理
+- **移除内容**:58行业务逻辑方法
+- **移除方法**:
+  - isUserTransaction()、isAdminOperation():交易类型判断
+  - isSellType()、isBuyType():交易方向判断
+  - getDirectionDescription():方向描述
+  - getCounterpartyUserId():交易对手方获取
+  - isUserBuyer()、isUserSeller():用户角色判断
+- **保留内容**:基础数据映射
+
+#### 1.4 MexPriceConfig模型清理
+- **移除内容**:68行业务逻辑方法
+- **移除方法**:
+  - getPriceRangeAttribute():价格区间计算
+  - isValidSellPrice()、isValidBuyPrice():价格验证
+  - isQuantityOverThreshold():数量阈值判断
+  - getPriceValidationError():错误信息生成
+- **保留内容**:配置数据字段
+
+#### 1.5 MexAdminOperation模型清理
+- **移除内容**:58行业务逻辑方法
+- **移除方法**:
+  - getQuantityChangeAttribute():库存变化计算
+  - isInjectOperation()、isRecycleOperation():操作类型判断
+  - getDirectionDescription():操作描述
+  - isStockChangeValid():数据验证
+  - getSummary():操作摘要
+- **保留内容**:操作记录数据
+
+### 2. 创建Repository层(5个仓库类)
+
+参考Fund模块的Repository设计模式,创建后台专用数据仓库:
+
+#### 2.1 设计原则
+- 继承UCore\DcatAdmin\Repository\EloquentRepository
+- 只包含$eloquentClass属性指定关联模型
+- 不包含任何业务逻辑方法
+- 专门为后台管理系统提供数据访问
+
+#### 2.2 创建的Repository类
+1. **MexOrderRepository**:订单数据仓库
+2. **MexWarehouseRepository**:系统仓库数据仓库
+3. **MexTransactionRepository**:成交记录数据仓库
+4. **MexPriceConfigRepository**:价格配置数据仓库
+5. **MexAdminOperationRepository**:管理员操作记录数据仓库
+
+#### 2.3 Repository特点
+- 每个类只有15-20行代码
+- 包含完整的类注释和说明
+- 遵循PSR-4命名规范
+- 为后台管理系统提供标准化数据访问
+
+### 3. 创建Service层(6个服务类)
+
+#### 3.1 设计原则
+- 提供对外服务接口
+- 使用静态方法调用Logic层
+- 不包含具体业务逻辑实现
+- 作为其他模块调用的入口
+
+#### 3.2 创建的Service类
+
+1. **MexOrderService**:订单服务
+   - createSellOrder():创建卖出订单
+   - createBuyOrder():创建买入订单
+   - cancelOrder():取消订单
+   - getUserOrders():获取用户订单列表
+   - getOrderDetail():获取订单详情
+   - getPendingBuyOrders():获取待撮合买入订单
+
+2. **MexWarehouseService**:仓库服务
+   - getItemStock():获取商品库存信息
+   - getItemsStock():获取多个商品库存
+   - checkStockSufficient():检查库存充足性
+   - getAvailableItems():获取有库存商品
+   - getWarehouseStats():获取仓库统计
+
+3. **MexTransactionService**:成交记录服务
+   - getPublicTransactions():获取交易大厅记录
+   - getUserTransactions():获取用户成交记录
+   - getItemTransactionStats():获取商品成交统计
+   - getMarketStats():获取市场统计
+   - getLatestPrice():获取最新成交价格
+
+4. **MexPriceConfigService**:价格配置服务
+   - getItemPriceConfig():获取商品价格配置
+   - getItemsPriceConfig():获取多个商品配置
+   - validateSellPrice():验证卖出价格
+   - validateBuyPrice():验证买入价格
+   - validateOrderQuantity():验证订单数量
+   - getEnabledConfigs():获取启用配置
+
+5. **MexAdminService**:管理员服务
+   - injectItem():物品注入操作
+   - recycleItem():物品回收操作
+   - getAdminOperations():获取操作记录
+   - getAdminOperationStats():获取操作统计
+
+6. **MexMatchService**:撮合服务
+   - executeMatch():执行撮合任务
+   - executeItemMatch():执行单商品撮合
+   - getMatchStats():获取撮合统计
+   - checkMatchConditions():检查撮合条件
+
+### 4. 开始创建Logic层
+
+#### 4.1 MexOrderLogic(订单核心逻辑)
+- **createSellOrder()**:创建卖出订单
+  - 价格配置验证
+  - 卖出价格验证(≤最低价)
+  - 立即处理卖出订单
+- **createBuyOrder()**:创建买入订单
+  - 价格配置验证
+  - 买入价格验证(≥最高价)
+  - 数量保护阈值验证
+  - 资金冻结处理
+- **cancelOrder()**:取消订单
+- **getUserOrders()**:获取用户订单列表
+- **getOrderDetail()**:获取订单详情
+- **getPendingBuyOrders()**:获取待撮合买入订单
+
+#### 4.2 Logic层特点
+- 包含完整的业务逻辑实现
+- 详细的参数验证和错误处理
+- 使用数据库事务确保数据一致性
+- 返回标准化的结果格式
+
+## 架构设计特点
+
+### 1. 职责分离
+- **Model层**:纯数据模型,只包含字段映射和类型转换
+- **Repository层**:后台专用数据访问,无业务逻辑
+- **Service层**:对外接口,调用Logic层处理业务
+- **Logic层**:核心业务逻辑处理
+
+### 2. 设计模式
+- 参考Fund模块的成熟设计模式
+- 遵循单一职责原则
+- 确保代码可维护性和扩展性
+
+### 3. 类型安全
+- 使用枚举类型确保数据安全
+- 完整的参数类型声明
+- 标准化的返回值格式
+
+## 代码统计
+
+### 移除的代码
+| Model | 移除行数 | 移除方法数 | 主要内容 |
+|-------|----------|------------|----------|
+| MexOrder | 47行 | 8个方法 | 订单状态判断、数量计算 |
+| MexWarehouse | 62行 | 8个方法 | 库存计算、操作方法 |
+| MexTransaction | 58行 | 8个方法 | 交易分析、角色判断 |
+| MexPriceConfig | 68行 | 8个方法 | 价格验证、错误处理 |
+| MexAdminOperation | 58行 | 8个方法 | 操作分析、数据验证 |
+| **总计** | **293行** | **40个方法** | **业务逻辑完全移除** |
+
+### 新增的代码
+| 层级 | 文件数 | 总行数 | 平均行数 |
+|------|--------|--------|----------|
+| Repository | 5个 | 100行 | 20行 |
+| Service | 6个 | 420行 | 70行 |
+| Logic | 1个 | 180行 | 180行 |
+| **总计** | **12个** | **700行** | **58行** |
+
+## 技术亮点
+
+### 1. 架构清晰
+- 严格的分层架构
+- 明确的职责划分
+- 标准化的接口设计
+
+### 2. 业务完整
+- 完整的订单创建流程
+- 详细的价格验证机制
+- 标准化的错误处理
+
+### 3. 扩展性强
+- Service层易于扩展
+- Logic层支持复杂业务
+- Repository层支持后台管理
+
+## 文件变更记录
+
+### 修改文件
+- 5个Model文件:移除业务逻辑,保持纯数据模型
+
+### 新增文件
+- 5个Repository文件:后台专用数据仓库
+- 6个Service文件:对外服务接口
+- 1个Logic文件:核心业务逻辑
+
+### Git提交信息
+```
+移除Model业务逻辑并创建Repository和Service层
+
+## 主要更新
+### 1. 移除Model中的业务逻辑
+- 移除293行业务逻辑代码,40个业务方法
+- 保持纯数据模型,专注数据映射和类型转换
+
+### 2. 创建Repository层(5个仓库类)
+- 参考Fund模块设计,后台专用数据仓库
+- 继承EloquentRepository,无业务逻辑
+
+### 3. 创建Service层(6个服务类)
+- 提供对外服务接口,调用Logic层处理业务逻辑
+- 包含订单、仓库、成交、价格、管理员、撮合服务
+
+### 4. 开始创建Logic层
+- MexOrderLogic:订单核心业务逻辑
+- 包含完整的验证和处理流程
+
+遵循用户要求的设计模式,确保职责分离和代码可维护性
+```
+
+## 任务成果
+1. **架构重构完成**:成功移除Model中的业务逻辑,建立清晰的分层架构
+2. **Repository层建立**:创建5个后台专用数据仓库,遵循Fund模块设计模式
+3. **Service层完善**:创建6个服务类,提供完整的对外接口
+4. **Logic层开始**:实现订单核心业务逻辑,包含完整的验证机制
+
+## 后续建议
+1. 继续完善Logic层的其他业务逻辑类
+2. 实现撮合算法和账户流转机制
+3. 创建后台管理控制器
+4. 开发计划任务处理撮合逻辑
+
+---
+**任务完成时间**:2025年06月11日 21:21  
+**文档状态**:已完成并提交到Git仓库

+ 209 - 0
app/Module/Mex/Commands/MexMatchCommand.php

@@ -0,0 +1,209 @@
+<?php
+
+namespace App\Module\Mex\Commands;
+
+use App\Module\Mex\Services\MexMatchService;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 农贸市场撮合计划任务
+ * 
+ * 定时执行撮合任务,处理待撮合的买入订单
+ */
+class MexMatchCommand extends Command
+{
+    /**
+     * 命令签名
+     *
+     * @var string
+     */
+    protected $signature = 'mex:match 
+                            {--item= : 指定商品ID,不指定则处理所有商品}
+                            {--batch=100 : 批处理大小,默认100}
+                            {--dry-run : 试运行模式,不执行实际撮合}';
+
+    /**
+     * 命令描述
+     *
+     * @var string
+     */
+    protected $description = '执行农贸市场撮合任务';
+
+    /**
+     * 执行命令
+     *
+     * @return int
+     */
+    public function handle(): int
+    {
+        $startTime = microtime(true);
+        
+        $this->info('开始执行农贸市场撮合任务...');
+        
+        // 获取命令参数
+        $itemId = $this->option('item') ? (int)$this->option('item') : null;
+        $batchSize = (int)$this->option('batch');
+        $dryRun = $this->option('dry-run');
+        
+        if ($dryRun) {
+            $this->warn('试运行模式:不会执行实际的撮合操作');
+            return $this->handleDryRun($itemId, $batchSize);
+        }
+        
+        try {
+            // 显示撮合前统计
+            $this->showPreMatchStats();
+            
+            // 执行撮合
+            $result = MexMatchService::executeMatch($itemId, $batchSize);
+            
+            // 显示撮合结果
+            $this->showMatchResult($result);
+            
+            // 显示撮合后统计
+            $this->showPostMatchStats();
+            
+            $endTime = microtime(true);
+            $totalTime = round(($endTime - $startTime) * 1000, 2);
+            
+            $this->info("撮合任务执行完成,总耗时: {$totalTime}ms");
+            
+            return $result['success'] ? Command::SUCCESS : Command::FAILURE;
+            
+        } catch (\Exception $e) {
+            $this->error('撮合任务执行失败: ' . $e->getMessage());
+            Log::error('MexMatchCommand执行失败', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+            
+            return Command::FAILURE;
+        }
+    }
+    
+    /**
+     * 处理试运行模式
+     *
+     * @param int|null $itemId
+     * @param int $batchSize
+     * @return int
+     */
+    private function handleDryRun(?int $itemId, int $batchSize): int
+    {
+        $this->info('=== 试运行模式 ===');
+        
+        // 显示当前统计
+        $stats = MexMatchService::getMatchStats();
+        
+        $this->table([
+            '待撮合订单数',
+            '涉及商品数',
+            '待撮合数量',
+            '待撮合金额',
+            '有库存商品数'
+        ], [[
+            $stats['pending_orders'],
+            $stats['pending_items'],
+            $stats['pending_quantity'],
+            $stats['pending_amount'],
+            $stats['available_items']
+        ]]);
+        
+        if ($itemId) {
+            $this->info("指定处理商品ID: {$itemId}");
+            $conditionCheck = MexMatchService::checkMatchConditions($itemId);
+            
+            if ($conditionCheck['can_match']) {
+                $this->info("✓ 商品 {$itemId} 满足撮合条件");
+                $this->info("  - 待撮合订单: {$conditionCheck['pending_orders']} 个");
+            } else {
+                $this->warn("✗ 商品 {$itemId} 不满足撮合条件: {$conditionCheck['message']}");
+            }
+        } else {
+            $this->info('将处理所有符合条件的商品');
+        }
+        
+        $this->info("批处理大小: {$batchSize}");
+        $this->info('试运行完成,未执行实际撮合操作');
+        
+        return Command::SUCCESS;
+    }
+    
+    /**
+     * 显示撮合前统计
+     */
+    private function showPreMatchStats(): void
+    {
+        $this->info('=== 撮合前统计 ===');
+        
+        $stats = MexMatchService::getMatchStats();
+        
+        $this->table([
+            '待撮合订单数',
+            '涉及商品数',
+            '待撮合数量',
+            '待撮合金额',
+            '有库存商品数'
+        ], [[
+            $stats['pending_orders'],
+            $stats['pending_items'],
+            $stats['pending_quantity'],
+            $stats['pending_amount'],
+            $stats['available_items']
+        ]]);
+    }
+    
+    /**
+     * 显示撮合结果
+     *
+     * @param array $result
+     */
+    private function showMatchResult(array $result): void
+    {
+        $this->info('=== 撮合结果 ===');
+        
+        if ($result['success']) {
+            $this->info("✓ 撮合成功");
+            $this->info("  - 处理商品: " . count($result['processed_items']) . " 个");
+            $this->info("  - 撮合订单: {$result['total_matched']} 个");
+            $this->info("  - 成交金额: {$result['total_amount']}");
+            $this->info("  - 执行时间: {$result['execution_time_ms']}ms");
+            
+            if (!empty($result['processed_items'])) {
+                $this->info("  - 处理的商品ID: " . implode(', ', $result['processed_items']));
+            }
+            
+            if (!empty($result['errors'])) {
+                $this->warn('部分商品处理出现错误:');
+                foreach ($result['errors'] as $error) {
+                    $this->warn("  - {$error}");
+                }
+            }
+        } else {
+            $this->error("✗ 撮合失败: {$result['message']}");
+        }
+    }
+    
+    /**
+     * 显示撮合后统计
+     */
+    private function showPostMatchStats(): void
+    {
+        $this->info('=== 撮合后统计 ===');
+        
+        $stats = MexMatchService::getMatchStats();
+        
+        $this->table([
+            '剩余待撮合订单',
+            '今日撮合订单',
+            '今日成交数量',
+            '今日成交金额'
+        ], [[
+            $stats['pending_orders'],
+            $stats['today_matched'],
+            $stats['today_quantity'],
+            $stats['today_amount']
+        ]]);
+    }
+}

+ 305 - 0
app/Module/Mex/Logic/MexAdminLogic.php

@@ -0,0 +1,305 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexAdminOperation;
+use App\Module\Mex\Models\MexWarehouse;
+use App\Module\Mex\Models\MexTransaction;
+use App\Module\Mex\Enums\AdminOperationType;
+use App\Module\Mex\Enums\TransactionType;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 农贸市场管理员操作逻辑
+ * 
+ * 处理管理员操作相关的核心业务逻辑
+ */
+class MexAdminLogic
+{
+    /**
+     * 仓库账户ID
+     */
+    private const WAREHOUSE_USER_ID = 15;
+    
+    /**
+     * 调控账户ID
+     */
+    private const CONTROL_USER_ID = 16;
+
+    /**
+     * 物品注入(增加市场供应)
+     * 
+     * @param int $adminUserId 管理员用户ID
+     * @param int $itemId 商品ID
+     * @param int $quantity 数量
+     * @param string $price 价格
+     * @param string|null $remark 备注
+     * @return array 操作结果
+     */
+    public static function injectItem(int $adminUserId, int $itemId, int $quantity, string $price, ?string $remark = null): array
+    {
+        if ($quantity <= 0) {
+            return ['success' => false, 'message' => '注入数量必须大于0'];
+        }
+
+        if (bccomp($price, '0', 5) <= 0) {
+            return ['success' => false, 'message' => '注入价格必须大于0'];
+        }
+
+        $totalAmount = bcmul($price, $quantity, 5);
+
+        try {
+            return DB::transaction(function () use ($adminUserId, $itemId, $quantity, $price, $totalAmount, $remark) {
+                // 获取操作前的仓库数量
+                $warehouse = MexWarehouse::firstOrCreate(
+                    ['item_id' => $itemId],
+                    [
+                        'quantity' => 0,
+                        'total_buy_amount' => '0.00000',
+                        'total_sell_amount' => '0.00000',
+                        'total_buy_quantity' => 0,
+                        'total_sell_quantity' => 0,
+                    ]
+                );
+
+                $beforeQuantity = $warehouse->quantity;
+
+                // 更新仓库库存(注入操作相当于系统买入)
+                $warehouse->quantity += $quantity;
+                $warehouse->total_buy_quantity += $quantity;
+                $warehouse->total_buy_amount = bcadd($warehouse->total_buy_amount, $totalAmount, 5);
+                $warehouse->last_transaction_at = now();
+                $warehouse->save();
+
+                $afterQuantity = $warehouse->quantity;
+
+                // 创建成交记录
+                $transaction = MexTransaction::create([
+                    'buy_order_id' => null,
+                    'sell_order_id' => null,
+                    'buyer_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为买方
+                    'seller_id' => self::CONTROL_USER_ID, // 调控账户作为卖方
+                    'item_id' => $itemId,
+                    'quantity' => $quantity,
+                    'price' => $price,
+                    'total_amount' => $totalAmount,
+                    'transaction_type' => TransactionType::ADMIN_INJECT,
+                    'is_admin_operation' => true,
+                    'admin_user_id' => $adminUserId,
+                ]);
+
+                // 创建管理员操作记录
+                $operation = MexAdminOperation::create([
+                    'admin_user_id' => $adminUserId,
+                    'operation_type' => AdminOperationType::INJECT,
+                    'item_id' => $itemId,
+                    'quantity' => $quantity,
+                    'price' => $price,
+                    'total_amount' => $totalAmount,
+                    'before_warehouse_quantity' => $beforeQuantity,
+                    'after_warehouse_quantity' => $afterQuantity,
+                    'transaction_id' => $transaction->id,
+                    'remark' => $remark,
+                ]);
+
+                return [
+                    'success' => true,
+                    'message' => '物品注入成功',
+                    'operation_id' => $operation->id,
+                    'transaction_id' => $transaction->id,
+                    'before_quantity' => $beforeQuantity,
+                    'after_quantity' => $afterQuantity,
+                    'injected_quantity' => $quantity,
+                ];
+            });
+        } catch (\Exception $e) {
+            return ['success' => false, 'message' => '物品注入失败:' . $e->getMessage()];
+        }
+    }
+
+    /**
+     * 物品回收(减少市场库存)
+     * 
+     * @param int $adminUserId 管理员用户ID
+     * @param int $itemId 商品ID
+     * @param int $quantity 数量
+     * @param string $price 价格
+     * @param string|null $remark 备注
+     * @return array 操作结果
+     */
+    public static function recycleItem(int $adminUserId, int $itemId, int $quantity, string $price, ?string $remark = null): array
+    {
+        if ($quantity <= 0) {
+            return ['success' => false, 'message' => '回收数量必须大于0'];
+        }
+
+        if (bccomp($price, '0', 5) <= 0) {
+            return ['success' => false, 'message' => '回收价格必须大于0'];
+        }
+
+        $totalAmount = bcmul($price, $quantity, 5);
+
+        try {
+            return DB::transaction(function () use ($adminUserId, $itemId, $quantity, $price, $totalAmount, $remark) {
+                // 检查仓库库存
+                $warehouse = MexWarehouse::where('item_id', $itemId)->first();
+                
+                if (!$warehouse || $warehouse->quantity < $quantity) {
+                    throw new \Exception('仓库库存不足,无法回收');
+                }
+
+                $beforeQuantity = $warehouse->quantity;
+
+                // 更新仓库库存(回收操作相当于系统卖出)
+                $warehouse->quantity -= $quantity;
+                $warehouse->total_sell_quantity += $quantity;
+                $warehouse->total_sell_amount = bcadd($warehouse->total_sell_amount, $totalAmount, 5);
+                $warehouse->last_transaction_at = now();
+                $warehouse->save();
+
+                $afterQuantity = $warehouse->quantity;
+
+                // 创建成交记录
+                $transaction = MexTransaction::create([
+                    'buy_order_id' => null,
+                    'sell_order_id' => null,
+                    'buyer_id' => self::CONTROL_USER_ID, // 调控账户作为买方
+                    'seller_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为卖方
+                    'item_id' => $itemId,
+                    'quantity' => $quantity,
+                    'price' => $price,
+                    'total_amount' => $totalAmount,
+                    'transaction_type' => TransactionType::ADMIN_RECYCLE,
+                    'is_admin_operation' => true,
+                    'admin_user_id' => $adminUserId,
+                ]);
+
+                // 创建管理员操作记录
+                $operation = MexAdminOperation::create([
+                    'admin_user_id' => $adminUserId,
+                    'operation_type' => AdminOperationType::RECYCLE,
+                    'item_id' => $itemId,
+                    'quantity' => $quantity,
+                    'price' => $price,
+                    'total_amount' => $totalAmount,
+                    'before_warehouse_quantity' => $beforeQuantity,
+                    'after_warehouse_quantity' => $afterQuantity,
+                    'transaction_id' => $transaction->id,
+                    'remark' => $remark,
+                ]);
+
+                return [
+                    'success' => true,
+                    'message' => '物品回收成功',
+                    'operation_id' => $operation->id,
+                    'transaction_id' => $transaction->id,
+                    'before_quantity' => $beforeQuantity,
+                    'after_quantity' => $afterQuantity,
+                    'recycled_quantity' => $quantity,
+                ];
+            });
+        } catch (\Exception $e) {
+            return ['success' => false, 'message' => '物品回收失败:' . $e->getMessage()];
+        }
+    }
+
+    /**
+     * 获取管理员操作记录
+     * 
+     * @param int $page 页码
+     * @param int $pageSize 每页数量
+     * @param int|null $adminUserId 管理员用户ID筛选
+     * @param AdminOperationType|null $operationType 操作类型筛选
+     * @return array 操作记录列表
+     */
+    public static function getAdminOperations(int $page = 1, int $pageSize = 20, ?int $adminUserId = null, ?AdminOperationType $operationType = null): array
+    {
+        $query = MexAdminOperation::query()->orderBy('created_at', 'desc');
+
+        if ($adminUserId) {
+            $query->where('admin_user_id', $adminUserId);
+        }
+
+        if ($operationType) {
+            $query->where('operation_type', $operationType);
+        }
+
+        $operations = $query->paginate($pageSize, ['*'], 'page', $page);
+
+        $result = [];
+        foreach ($operations->items() as $operation) {
+            $result[] = [
+                'id' => $operation->id,
+                'admin_user_id' => $operation->admin_user_id,
+                'operation_type' => $operation->operation_type->value,
+                'operation_type_desc' => $operation->operation_type->getDescription(),
+                'item_id' => $operation->item_id,
+                'quantity' => $operation->quantity,
+                'price' => $operation->price,
+                'total_amount' => $operation->total_amount,
+                'before_warehouse_quantity' => $operation->before_warehouse_quantity,
+                'after_warehouse_quantity' => $operation->after_warehouse_quantity,
+                'quantity_change' => $operation->after_warehouse_quantity - $operation->before_warehouse_quantity,
+                'transaction_id' => $operation->transaction_id,
+                'remark' => $operation->remark,
+                'created_at' => $operation->created_at,
+            ];
+        }
+
+        return [
+            'operations' => $result,
+            'total' => $operations->total(),
+            'page' => $page,
+            'page_size' => $pageSize,
+        ];
+    }
+
+    /**
+     * 获取管理员操作统计
+     * 
+     * @param int $days 统计天数
+     * @return array 统计信息
+     */
+    public static function getAdminOperationStats(int $days = 7): array
+    {
+        $startDate = now()->subDays($days)->startOfDay();
+        
+        $stats = MexAdminOperation::where('created_at', '>=', $startDate)
+            ->selectRaw('
+                COUNT(*) as total_operations,
+                COUNT(CASE WHEN operation_type = ? THEN 1 END) as inject_count,
+                COUNT(CASE WHEN operation_type = ? THEN 1 END) as recycle_count,
+                SUM(CASE WHEN operation_type = ? THEN quantity ELSE 0 END) as inject_quantity,
+                SUM(CASE WHEN operation_type = ? THEN quantity ELSE 0 END) as recycle_quantity,
+                SUM(CASE WHEN operation_type = ? THEN total_amount ELSE 0 END) as inject_amount,
+                SUM(CASE WHEN operation_type = ? THEN total_amount ELSE 0 END) as recycle_amount,
+                COUNT(DISTINCT admin_user_id) as active_admins,
+                COUNT(DISTINCT item_id) as affected_items
+            ', [
+                AdminOperationType::INJECT->value,
+                AdminOperationType::RECYCLE->value,
+                AdminOperationType::INJECT->value,
+                AdminOperationType::RECYCLE->value,
+                AdminOperationType::INJECT->value,
+                AdminOperationType::RECYCLE->value,
+            ])
+            ->first();
+
+        return [
+            'days' => $days,
+            'total_operations' => $stats->total_operations ?? 0,
+            'inject_count' => $stats->inject_count ?? 0,
+            'recycle_count' => $stats->recycle_count ?? 0,
+            'inject_quantity' => $stats->inject_quantity ?? 0,
+            'recycle_quantity' => $stats->recycle_quantity ?? 0,
+            'inject_amount' => $stats->inject_amount ?? '0.00000',
+            'recycle_amount' => $stats->recycle_amount ?? '0.00000',
+            'net_quantity' => ($stats->inject_quantity ?? 0) - ($stats->recycle_quantity ?? 0),
+            'net_amount' => bcsub($stats->inject_amount ?? '0', $stats->recycle_amount ?? '0', 5),
+            'active_admins' => $stats->active_admins ?? 0,
+            'affected_items' => $stats->affected_items ?? 0,
+            'start_date' => $startDate,
+            'end_date' => now(),
+        ];
+    }
+}

+ 347 - 0
app/Module/Mex/Logic/MexMatchLogic.php

@@ -0,0 +1,347 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexOrder;
+use App\Module\Mex\Models\MexWarehouse;
+use App\Module\Mex\Models\MexTransaction;
+use App\Module\Mex\Models\MexPriceConfig;
+use App\Module\Mex\Enums\OrderType;
+use App\Module\Mex\Enums\OrderStatus;
+use App\Module\Mex\Enums\TransactionType;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 农贸市场撮合逻辑
+ * 
+ * 处理撮合相关的核心业务逻辑
+ */
+class MexMatchLogic
+{
+    /**
+     * 仓库账户ID
+     */
+    private const WAREHOUSE_USER_ID = 15;
+
+    /**
+     * 执行撮合任务
+     * 
+     * @param int|null $itemId 指定商品ID,null表示处理所有商品
+     * @param int $batchSize 批处理大小
+     * @return array 撮合结果
+     */
+    public static function executeMatch(?int $itemId = null, int $batchSize = 100): array
+    {
+        $startTime = microtime(true);
+        $totalMatched = 0;
+        $totalAmount = '0.00000';
+        $processedItems = [];
+        $errors = [];
+
+        try {
+            if ($itemId) {
+                // 处理指定商品
+                $result = self::executeItemMatch($itemId, $batchSize);
+                $processedItems[] = $itemId;
+                $totalMatched += $result['matched_orders'];
+                $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
+                if (!$result['success']) {
+                    $errors[] = "商品 {$itemId}: " . $result['message'];
+                }
+            } else {
+                // 处理所有有待撮合订单的商品
+                $itemIds = MexOrder::where('order_type', OrderType::BUY)
+                    ->where('status', OrderStatus::PENDING)
+                    ->distinct()
+                    ->pluck('item_id')
+                    ->toArray();
+
+                foreach ($itemIds as $currentItemId) {
+                    $result = self::executeItemMatch($currentItemId, $batchSize);
+                    $processedItems[] = $currentItemId;
+                    $totalMatched += $result['matched_orders'];
+                    $totalAmount = bcadd($totalAmount, $result['total_amount'], 5);
+                    
+                    if (!$result['success']) {
+                        $errors[] = "商品 {$currentItemId}: " . $result['message'];
+                    }
+                }
+            }
+
+            $endTime = microtime(true);
+            $executionTime = round(($endTime - $startTime) * 1000, 2); // 毫秒
+
+            Log::info('Mex撮合任务执行完成', [
+                'processed_items' => $processedItems,
+                'total_matched' => $totalMatched,
+                'total_amount' => $totalAmount,
+                'execution_time_ms' => $executionTime,
+                'errors' => $errors,
+            ]);
+
+            return [
+                'success' => true,
+                'message' => '撮合任务执行完成',
+                'processed_items' => $processedItems,
+                'total_matched' => $totalMatched,
+                'total_amount' => $totalAmount,
+                'execution_time_ms' => $executionTime,
+                'errors' => $errors,
+            ];
+        } catch (\Exception $e) {
+            Log::error('Mex撮合任务执行失败', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '撮合任务执行失败:' . $e->getMessage(),
+                'processed_items' => $processedItems,
+                'total_matched' => $totalMatched,
+                'total_amount' => $totalAmount,
+            ];
+        }
+    }
+
+    /**
+     * 执行单个商品的撮合
+     * 
+     * @param int $itemId 商品ID
+     * @param int $batchSize 批处理大小
+     * @return array 撮合结果
+     */
+    public static function executeItemMatch(int $itemId, int $batchSize = 100): array
+    {
+        try {
+            return DB::transaction(function () use ($itemId, $batchSize) {
+                // 检查撮合条件
+                $conditionCheck = self::checkMatchConditions($itemId);
+                if (!$conditionCheck['can_match']) {
+                    return [
+                        'success' => false,
+                        'message' => $conditionCheck['message'],
+                        'matched_orders' => 0,
+                        'total_amount' => '0.00000',
+                    ];
+                }
+
+                $warehouse = $conditionCheck['warehouse'];
+                $priceConfig = $conditionCheck['price_config'];
+
+                // 获取待撮合的买入订单(三级排序)
+                $buyOrders = MexOrder::where('item_id', $itemId)
+                    ->where('order_type', OrderType::BUY)
+                    ->where('status', OrderStatus::PENDING)
+                    ->where('quantity', '<=', $priceConfig->protection_threshold) // 过滤大额订单
+                    ->orderBy('price', 'desc')      // 价格优先(高价优先)
+                    ->orderBy('created_at', 'asc')  // 时间优先(早下单优先)
+                    ->orderBy('quantity', 'asc')    // 数量优先(小单优先)
+                    ->limit($batchSize)
+                    ->get();
+
+                if ($buyOrders->isEmpty()) {
+                    return [
+                        'success' => true,
+                        'message' => '没有符合条件的买入订单',
+                        'matched_orders' => 0,
+                        'total_amount' => '0.00000',
+                    ];
+                }
+
+                $matchedOrders = 0;
+                $totalAmount = '0.00000';
+                $currentStock = $warehouse->quantity;
+
+                foreach ($buyOrders as $order) {
+                    // 检查库存是否充足(整单匹配原则)
+                    if ($currentStock < $order->quantity) {
+                        continue; // 库存不足,跳过此订单
+                    }
+
+                    // 执行撮合
+                    $matchResult = self::executeOrderMatch($order, $warehouse);
+                    if ($matchResult['success']) {
+                        $matchedOrders++;
+                        $totalAmount = bcadd($totalAmount, $matchResult['total_amount'], 5);
+                        $currentStock -= $order->quantity;
+                        
+                        // 更新仓库对象的库存(用于后续订单判断)
+                        $warehouse->quantity = $currentStock;
+                    }
+                }
+
+                return [
+                    'success' => true,
+                    'message' => "成功撮合 {$matchedOrders} 个订单",
+                    'matched_orders' => $matchedOrders,
+                    'total_amount' => $totalAmount,
+                ];
+            });
+        } catch (\Exception $e) {
+            return [
+                'success' => false,
+                'message' => '撮合执行失败:' . $e->getMessage(),
+                'matched_orders' => 0,
+                'total_amount' => '0.00000',
+            ];
+        }
+    }
+
+    /**
+     * 执行单个订单的撮合
+     * 
+     * @param MexOrder $order 买入订单
+     * @param MexWarehouse $warehouse 仓库信息
+     * @return array 撮合结果
+     */
+    private static function executeOrderMatch(MexOrder $order, MexWarehouse $warehouse): array
+    {
+        try {
+            // 计算成交金额
+            $totalAmount = bcmul($order->price, $order->quantity, 5);
+
+            // 更新订单状态
+            $order->update([
+                'status' => OrderStatus::COMPLETED,
+                'completed_quantity' => $order->quantity,
+                'completed_amount' => $totalAmount,
+                'completed_at' => now(),
+            ]);
+
+            // 更新仓库库存
+            $warehouse->quantity -= $order->quantity;
+            $warehouse->total_sell_quantity += $order->quantity;
+            $warehouse->total_sell_amount = bcadd($warehouse->total_sell_amount, $totalAmount, 5);
+            $warehouse->last_transaction_at = now();
+            $warehouse->save();
+
+            // 创建成交记录
+            $transaction = MexTransaction::create([
+                'buy_order_id' => $order->id,
+                'sell_order_id' => null,
+                'buyer_id' => $order->user_id,
+                'seller_id' => self::WAREHOUSE_USER_ID, // 仓库账户作为卖方
+                'item_id' => $order->item_id,
+                'quantity' => $order->quantity,
+                'price' => $order->price,
+                'total_amount' => $totalAmount,
+                'transaction_type' => TransactionType::USER_BUY,
+                'is_admin_operation' => false,
+            ]);
+
+            // TODO: 这里应该调用账户流转逻辑
+            // 1. 用户冻结资金转入仓库账户
+            // 2. 仓库账户物品转出到用户账户
+
+            return [
+                'success' => true,
+                'message' => '订单撮合成功',
+                'order_id' => $order->id,
+                'transaction_id' => $transaction->id,
+                'total_amount' => $totalAmount,
+            ];
+        } catch (\Exception $e) {
+            return [
+                'success' => false,
+                'message' => '订单撮合失败:' . $e->getMessage(),
+                'order_id' => $order->id,
+                'total_amount' => '0.00000',
+            ];
+        }
+    }
+
+    /**
+     * 检查撮合条件
+     * 
+     * @param int $itemId 商品ID
+     * @return array 检查结果
+     */
+    public static function checkMatchConditions(int $itemId): array
+    {
+        // 检查价格配置
+        $priceConfig = MexPriceConfig::where('item_id', $itemId)->where('is_enabled', true)->first();
+        if (!$priceConfig) {
+            return [
+                'can_match' => false,
+                'message' => '商品未配置价格信息或已禁用',
+            ];
+        }
+
+        // 检查仓库库存
+        $warehouse = MexWarehouse::where('item_id', $itemId)->first();
+        if (!$warehouse || $warehouse->quantity <= 0) {
+            return [
+                'can_match' => false,
+                'message' => '仓库库存不足',
+            ];
+        }
+
+        // 检查是否有待撮合的买入订单
+        $pendingBuyOrders = MexOrder::where('item_id', $itemId)
+            ->where('order_type', OrderType::BUY)
+            ->where('status', OrderStatus::PENDING)
+            ->where('quantity', '<=', $priceConfig->protection_threshold)
+            ->count();
+
+        if ($pendingBuyOrders === 0) {
+            return [
+                'can_match' => false,
+                'message' => '没有符合条件的待撮合买入订单',
+            ];
+        }
+
+        return [
+            'can_match' => true,
+            'message' => '撮合条件满足',
+            'warehouse' => $warehouse,
+            'price_config' => $priceConfig,
+            'pending_orders' => $pendingBuyOrders,
+        ];
+    }
+
+    /**
+     * 获取撮合统计信息
+     * 
+     * @return array 统计信息
+     */
+    public static function getMatchStats(): array
+    {
+        // 获取待撮合订单统计
+        $pendingStats = MexOrder::where('order_type', OrderType::BUY)
+            ->where('status', OrderStatus::PENDING)
+            ->selectRaw('
+                COUNT(*) as total_pending,
+                COUNT(DISTINCT item_id) as pending_items,
+                SUM(quantity) as total_quantity,
+                SUM(total_amount) as total_amount
+            ')
+            ->first();
+
+        // 获取今日撮合统计
+        $todayStats = MexTransaction::where('transaction_type', TransactionType::USER_BUY)
+            ->whereDate('created_at', today())
+            ->selectRaw('
+                COUNT(*) as today_matched,
+                SUM(quantity) as today_quantity,
+                SUM(total_amount) as today_amount
+            ')
+            ->first();
+
+        // 获取有库存的商品数量
+        $availableItems = MexWarehouse::where('quantity', '>', 0)->count();
+
+        return [
+            'pending_orders' => $pendingStats->total_pending ?? 0,
+            'pending_items' => $pendingStats->pending_items ?? 0,
+            'pending_quantity' => $pendingStats->total_quantity ?? 0,
+            'pending_amount' => $pendingStats->total_amount ?? '0.00000',
+            'today_matched' => $todayStats->today_matched ?? 0,
+            'today_quantity' => $todayStats->today_quantity ?? 0,
+            'today_amount' => $todayStats->today_amount ?? '0.00000',
+            'available_items' => $availableItems,
+            'stats_time' => now(),
+        ];
+    }
+}

+ 324 - 0
app/Module/Mex/Logic/MexPriceConfigLogic.php

@@ -0,0 +1,324 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexPriceConfig;
+use Illuminate\Support\Facades\Cache;
+
+/**
+ * 农贸市场价格配置逻辑
+ * 
+ * 处理价格配置相关的核心业务逻辑
+ */
+class MexPriceConfigLogic
+{
+    /**
+     * 缓存键前缀
+     */
+    private const CACHE_PREFIX = 'mex_price_config:';
+    
+    /**
+     * 缓存时间(秒)
+     */
+    private const CACHE_TTL = 600; // 10分钟
+
+    /**
+     * 获取商品价格配置
+     * 
+     * @param int $itemId 商品ID
+     * @return array|null 价格配置
+     */
+    public static function getItemPriceConfig(int $itemId): ?array
+    {
+        $cacheKey = self::CACHE_PREFIX . $itemId;
+        
+        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($itemId) {
+            $config = MexPriceConfig::where('item_id', $itemId)->first();
+            
+            if (!$config) {
+                return null;
+            }
+
+            return [
+                'item_id' => $config->item_id,
+                'min_price' => $config->min_price,
+                'max_price' => $config->max_price,
+                'protection_threshold' => $config->protection_threshold,
+                'is_enabled' => $config->is_enabled,
+                'price_range' => $config->min_price . ' - ' . $config->max_price,
+                'price_range_width' => bcsub($config->max_price, $config->min_price, 5),
+                'created_at' => $config->created_at,
+                'updated_at' => $config->updated_at,
+            ];
+        });
+    }
+
+    /**
+     * 获取多个商品的价格配置
+     * 
+     * @param array $itemIds 商品ID数组
+     * @return array 价格配置列表
+     */
+    public static function getItemsPriceConfig(array $itemIds): array
+    {
+        $result = [];
+        
+        foreach ($itemIds as $itemId) {
+            $config = self::getItemPriceConfig($itemId);
+            if ($config) {
+                $result[$itemId] = $config;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * 验证卖出价格
+     * 
+     * @param int $itemId 商品ID
+     * @param string $price 价格
+     * @return array 验证结果
+     */
+    public static function validateSellPrice(int $itemId, string $price): array
+    {
+        $config = self::getItemPriceConfig($itemId);
+        
+        if (!$config) {
+            return [
+                'valid' => false,
+                'message' => '商品未配置价格信息',
+                'error_code' => 'CONFIG_NOT_FOUND'
+            ];
+        }
+
+        if (!$config['is_enabled']) {
+            return [
+                'valid' => false,
+                'message' => '商品价格配置已禁用',
+                'error_code' => 'CONFIG_DISABLED'
+            ];
+        }
+
+        if (bccomp($price, $config['min_price'], 5) > 0) {
+            return [
+                'valid' => false,
+                'message' => "卖出价格不能高于最低价 {$config['min_price']}",
+                'error_code' => 'PRICE_TOO_HIGH',
+                'min_price' => $config['min_price']
+            ];
+        }
+
+        return [
+            'valid' => true,
+            'message' => '价格验证通过',
+            'config' => $config
+        ];
+    }
+
+    /**
+     * 验证买入价格
+     * 
+     * @param int $itemId 商品ID
+     * @param string $price 价格
+     * @return array 验证结果
+     */
+    public static function validateBuyPrice(int $itemId, string $price): array
+    {
+        $config = self::getItemPriceConfig($itemId);
+        
+        if (!$config) {
+            return [
+                'valid' => false,
+                'message' => '商品未配置价格信息',
+                'error_code' => 'CONFIG_NOT_FOUND'
+            ];
+        }
+
+        if (!$config['is_enabled']) {
+            return [
+                'valid' => false,
+                'message' => '商品价格配置已禁用',
+                'error_code' => 'CONFIG_DISABLED'
+            ];
+        }
+
+        if (bccomp($price, $config['max_price'], 5) < 0) {
+            return [
+                'valid' => false,
+                'message' => "买入价格不能低于最高价 {$config['max_price']}",
+                'error_code' => 'PRICE_TOO_LOW',
+                'max_price' => $config['max_price']
+            ];
+        }
+
+        return [
+            'valid' => true,
+            'message' => '价格验证通过',
+            'config' => $config
+        ];
+    }
+
+    /**
+     * 验证订单数量
+     * 
+     * @param int $itemId 商品ID
+     * @param int $quantity 数量
+     * @return array 验证结果
+     */
+    public static function validateOrderQuantity(int $itemId, int $quantity): array
+    {
+        $config = self::getItemPriceConfig($itemId);
+        
+        if (!$config) {
+            return [
+                'valid' => false,
+                'message' => '商品未配置价格信息',
+                'error_code' => 'CONFIG_NOT_FOUND'
+            ];
+        }
+
+        if (!$config['is_enabled']) {
+            return [
+                'valid' => false,
+                'message' => '商品价格配置已禁用',
+                'error_code' => 'CONFIG_DISABLED'
+            ];
+        }
+
+        if ($quantity > $config['protection_threshold']) {
+            return [
+                'valid' => false,
+                'message' => "订单数量不能超过保护阈值 {$config['protection_threshold']}",
+                'error_code' => 'QUANTITY_OVER_THRESHOLD',
+                'protection_threshold' => $config['protection_threshold']
+            ];
+        }
+
+        return [
+            'valid' => true,
+            'message' => '数量验证通过',
+            'config' => $config
+        ];
+    }
+
+    /**
+     * 获取所有启用的价格配置
+     * 
+     * @return array 价格配置列表
+     */
+    public static function getEnabledConfigs(): array
+    {
+        $cacheKey = self::CACHE_PREFIX . 'enabled_all';
+        
+        return Cache::remember($cacheKey, self::CACHE_TTL, function () {
+            $configs = MexPriceConfig::where('is_enabled', true)
+                ->orderBy('item_id', 'asc')
+                ->get();
+
+            $result = [];
+            foreach ($configs as $config) {
+                $result[] = [
+                    'item_id' => $config->item_id,
+                    'min_price' => $config->min_price,
+                    'max_price' => $config->max_price,
+                    'protection_threshold' => $config->protection_threshold,
+                    'price_range' => $config->min_price . ' - ' . $config->max_price,
+                    'price_range_width' => bcsub($config->max_price, $config->min_price, 5),
+                ];
+            }
+
+            return $result;
+        });
+    }
+
+    /**
+     * 清除价格配置缓存
+     * 
+     * @param int|null $itemId 商品ID,null表示清除所有
+     * @return bool 操作结果
+     */
+    public static function clearCache(?int $itemId = null): bool
+    {
+        if ($itemId) {
+            $cacheKey = self::CACHE_PREFIX . $itemId;
+            return Cache::forget($cacheKey);
+        }
+
+        // 清除所有相关缓存
+        Cache::forget(self::CACHE_PREFIX . 'enabled_all');
+        
+        // 这里可以根据需要清除特定商品的缓存
+        // 由于无法直接获取所有缓存键,建议在配置更新时调用具体的清除方法
+        
+        return true;
+    }
+
+    /**
+     * 批量验证价格和数量
+     * 
+     * @param int $itemId 商品ID
+     * @param string $price 价格
+     * @param int $quantity 数量
+     * @param string $orderType 订单类型 (BUY/SELL)
+     * @return array 验证结果
+     */
+    public static function validateOrderParams(int $itemId, string $price, int $quantity, string $orderType): array
+    {
+        // 验证价格
+        if ($orderType === 'SELL') {
+            $priceResult = self::validateSellPrice($itemId, $price);
+        } else {
+            $priceResult = self::validateBuyPrice($itemId, $price);
+        }
+
+        if (!$priceResult['valid']) {
+            return $priceResult;
+        }
+
+        // 验证数量(只对买入订单验证)
+        if ($orderType === 'BUY') {
+            $quantityResult = self::validateOrderQuantity($itemId, $quantity);
+            if (!$quantityResult['valid']) {
+                return $quantityResult;
+            }
+        }
+
+        return [
+            'valid' => true,
+            'message' => '参数验证通过',
+            'config' => $priceResult['config']
+        ];
+    }
+
+    /**
+     * 获取商品价格建议
+     * 
+     * @param int $itemId 商品ID
+     * @return array 价格建议
+     */
+    public static function getPriceSuggestion(int $itemId): array
+    {
+        $config = self::getItemPriceConfig($itemId);
+        
+        if (!$config) {
+            return [
+                'has_suggestion' => false,
+                'message' => '商品未配置价格信息'
+            ];
+        }
+
+        // 获取最新成交价格作为参考
+        $latestPrice = MexTransactionLogic::getLatestPrice($itemId);
+
+        return [
+            'has_suggestion' => true,
+            'min_price' => $config['min_price'],
+            'max_price' => $config['max_price'],
+            'latest_price' => $latestPrice,
+            'sell_suggestion' => $config['min_price'],
+            'buy_suggestion' => $config['max_price'],
+            'protection_threshold' => $config['protection_threshold'],
+        ];
+    }
+}

+ 280 - 0
app/Module/Mex/Logic/MexTransactionLogic.php

@@ -0,0 +1,280 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexTransaction;
+use App\Module\Mex\Enums\TransactionType;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 农贸市场成交记录逻辑
+ * 
+ * 处理成交记录相关的核心业务逻辑
+ */
+class MexTransactionLogic
+{
+    /**
+     * 获取交易大厅成交记录
+     * 
+     * @param int $page 页码
+     * @param int $pageSize 每页数量
+     * @param int|null $itemId 商品ID筛选
+     * @return array 成交记录列表
+     */
+    public static function getPublicTransactions(int $page = 1, int $pageSize = 20, ?int $itemId = null): array
+    {
+        $query = MexTransaction::query()
+            ->where('is_admin_operation', false) // 只显示用户交易,不显示管理员操作
+            ->orderBy('created_at', 'desc');
+
+        if ($itemId) {
+            $query->where('item_id', $itemId);
+        }
+
+        $transactions = $query->paginate($pageSize, ['*'], 'page', $page);
+
+        $result = [];
+        foreach ($transactions->items() as $transaction) {
+            $result[] = [
+                'id' => $transaction->id,
+                'item_id' => $transaction->item_id,
+                'quantity' => $transaction->quantity,
+                'price' => $transaction->price,
+                'total_amount' => $transaction->total_amount,
+                'transaction_type' => $transaction->transaction_type->value,
+                'transaction_type_desc' => $transaction->transaction_type->getDescription(),
+                'created_at' => $transaction->created_at,
+            ];
+        }
+
+        return [
+            'transactions' => $result,
+            'total' => $transactions->total(),
+            'page' => $page,
+            'page_size' => $pageSize,
+        ];
+    }
+
+    /**
+     * 获取用户成交记录
+     * 
+     * @param int $userId 用户ID
+     * @param int $page 页码
+     * @param int $pageSize 每页数量
+     * @return array 成交记录列表
+     */
+    public static function getUserTransactions(int $userId, int $page = 1, int $pageSize = 20): array
+    {
+        $transactions = MexTransaction::where(function ($query) use ($userId) {
+            $query->where('buyer_id', $userId)
+                  ->orWhere('seller_id', $userId);
+        })
+        ->where('is_admin_operation', false)
+        ->orderBy('created_at', 'desc')
+        ->paginate($pageSize, ['*'], 'page', $page);
+
+        $result = [];
+        foreach ($transactions->items() as $transaction) {
+            $isUserBuyer = $transaction->buyer_id === $userId;
+            $isUserSeller = $transaction->seller_id === $userId;
+            
+            $result[] = [
+                'id' => $transaction->id,
+                'item_id' => $transaction->item_id,
+                'quantity' => $transaction->quantity,
+                'price' => $transaction->price,
+                'total_amount' => $transaction->total_amount,
+                'transaction_type' => $transaction->transaction_type->value,
+                'transaction_type_desc' => $transaction->transaction_type->getDescription(),
+                'user_role' => $isUserBuyer ? 'buyer' : ($isUserSeller ? 'seller' : 'unknown'),
+                'user_role_desc' => $isUserBuyer ? '买方' : ($isUserSeller ? '卖方' : '未知'),
+                'counterparty_id' => $isUserBuyer ? $transaction->seller_id : $transaction->buyer_id,
+                'created_at' => $transaction->created_at,
+            ];
+        }
+
+        return [
+            'transactions' => $result,
+            'total' => $transactions->total(),
+            'page' => $page,
+            'page_size' => $pageSize,
+        ];
+    }
+
+    /**
+     * 获取商品成交统计
+     * 
+     * @param int $itemId 商品ID
+     * @param int $days 统计天数
+     * @return array 统计信息
+     */
+    public static function getItemTransactionStats(int $itemId, int $days = 7): array
+    {
+        $startDate = now()->subDays($days)->startOfDay();
+        
+        $stats = MexTransaction::where('item_id', $itemId)
+            ->where('created_at', '>=', $startDate)
+            ->selectRaw('
+                COUNT(*) as total_transactions,
+                SUM(quantity) as total_quantity,
+                SUM(total_amount) as total_amount,
+                AVG(price) as avg_price,
+                MIN(price) as min_price,
+                MAX(price) as max_price
+            ')
+            ->first();
+
+        $latestPrice = self::getLatestPrice($itemId);
+
+        return [
+            'item_id' => $itemId,
+            'days' => $days,
+            'total_transactions' => $stats->total_transactions ?? 0,
+            'total_quantity' => $stats->total_quantity ?? 0,
+            'total_amount' => $stats->total_amount ?? '0.00000',
+            'avg_price' => $stats->avg_price ? number_format($stats->avg_price, 5) : '0.00000',
+            'min_price' => $stats->min_price ?? '0.00000',
+            'max_price' => $stats->max_price ?? '0.00000',
+            'latest_price' => $latestPrice,
+            'start_date' => $startDate,
+            'end_date' => now(),
+        ];
+    }
+
+    /**
+     * 获取市场成交统计
+     * 
+     * @param int $days 统计天数
+     * @return array 统计信息
+     */
+    public static function getMarketStats(int $days = 7): array
+    {
+        $startDate = now()->subDays($days)->startOfDay();
+        
+        $stats = MexTransaction::where('created_at', '>=', $startDate)
+            ->selectRaw('
+                COUNT(*) as total_transactions,
+                COUNT(DISTINCT item_id) as active_items,
+                SUM(quantity) as total_quantity,
+                SUM(total_amount) as total_amount,
+                AVG(price) as avg_price
+            ')
+            ->first();
+
+        // 按交易类型统计
+        $typeStats = MexTransaction::where('created_at', '>=', $startDate)
+            ->groupBy('transaction_type')
+            ->selectRaw('
+                transaction_type,
+                COUNT(*) as count,
+                SUM(quantity) as quantity,
+                SUM(total_amount) as amount
+            ')
+            ->get();
+
+        $typeStatsFormatted = [];
+        foreach ($typeStats as $typeStat) {
+            $typeStatsFormatted[$typeStat->transaction_type] = [
+                'count' => $typeStat->count,
+                'quantity' => $typeStat->quantity,
+                'amount' => $typeStat->amount,
+            ];
+        }
+
+        return [
+            'days' => $days,
+            'total_transactions' => $stats->total_transactions ?? 0,
+            'active_items' => $stats->active_items ?? 0,
+            'total_quantity' => $stats->total_quantity ?? 0,
+            'total_amount' => $stats->total_amount ?? '0.00000',
+            'avg_price' => $stats->avg_price ? number_format($stats->avg_price, 5) : '0.00000',
+            'type_stats' => $typeStatsFormatted,
+            'start_date' => $startDate,
+            'end_date' => now(),
+        ];
+    }
+
+    /**
+     * 获取商品最新成交价格
+     * 
+     * @param int $itemId 商品ID
+     * @return string|null 最新价格
+     */
+    public static function getLatestPrice(int $itemId): ?string
+    {
+        $transaction = MexTransaction::where('item_id', $itemId)
+            ->orderBy('created_at', 'desc')
+            ->first();
+
+        return $transaction ? $transaction->price : null;
+    }
+
+    /**
+     * 创建成交记录
+     * 
+     * @param array $data 成交数据
+     * @return MexTransaction|null 成交记录
+     */
+    public static function createTransaction(array $data): ?MexTransaction
+    {
+        try {
+            return MexTransaction::create([
+                'buy_order_id' => $data['buy_order_id'] ?? null,
+                'sell_order_id' => $data['sell_order_id'] ?? null,
+                'buyer_id' => $data['buyer_id'],
+                'seller_id' => $data['seller_id'],
+                'item_id' => $data['item_id'],
+                'quantity' => $data['quantity'],
+                'price' => $data['price'],
+                'total_amount' => $data['total_amount'],
+                'transaction_type' => $data['transaction_type'],
+                'is_admin_operation' => $data['is_admin_operation'] ?? false,
+                'admin_user_id' => $data['admin_user_id'] ?? null,
+            ]);
+        } catch (\Exception $e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取商品价格趋势
+     * 
+     * @param int $itemId 商品ID
+     * @param int $days 统计天数
+     * @return array 价格趋势数据
+     */
+    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')
+            ->select('price', 'created_at')
+            ->get();
+
+        $trend = [];
+        foreach ($transactions as $transaction) {
+            $date = $transaction->created_at->format('Y-m-d');
+            if (!isset($trend[$date])) {
+                $trend[$date] = [];
+            }
+            $trend[$date][] = $transaction->price;
+        }
+
+        // 计算每日平均价格
+        $result = [];
+        foreach ($trend as $date => $prices) {
+            $avgPrice = array_sum($prices) / count($prices);
+            $result[] = [
+                'date' => $date,
+                'avg_price' => number_format($avgPrice, 5),
+                'min_price' => min($prices),
+                'max_price' => max($prices),
+                'transaction_count' => count($prices),
+            ];
+        }
+
+        return $result;
+    }
+}

+ 220 - 0
app/Module/Mex/Logic/MexWarehouseLogic.php

@@ -0,0 +1,220 @@
+<?php
+
+namespace App\Module\Mex\Logic;
+
+use App\Module\Mex\Models\MexWarehouse;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 农贸市场仓库逻辑
+ * 
+ * 处理仓库相关的核心业务逻辑
+ */
+class MexWarehouseLogic
+{
+    /**
+     * 获取商品库存信息
+     * 
+     * @param int $itemId 商品ID
+     * @return array|null 库存信息
+     */
+    public static function getItemStock(int $itemId): ?array
+    {
+        $warehouse = MexWarehouse::where('item_id', $itemId)->first();
+        
+        if (!$warehouse) {
+            return null;
+        }
+
+        return [
+            'item_id' => $warehouse->item_id,
+            'quantity' => $warehouse->quantity,
+            'total_buy_amount' => $warehouse->total_buy_amount,
+            'total_sell_amount' => $warehouse->total_sell_amount,
+            'total_buy_quantity' => $warehouse->total_buy_quantity,
+            'total_sell_quantity' => $warehouse->total_sell_quantity,
+            'average_buy_price' => self::calculateAveragePrice($warehouse->total_buy_amount, $warehouse->total_buy_quantity),
+            'average_sell_price' => self::calculateAveragePrice($warehouse->total_sell_amount, $warehouse->total_sell_quantity),
+            'net_buy_quantity' => $warehouse->total_buy_quantity - $warehouse->total_sell_quantity,
+            'net_buy_amount' => bcsub($warehouse->total_buy_amount, $warehouse->total_sell_amount, 5),
+            'last_transaction_at' => $warehouse->last_transaction_at,
+        ];
+    }
+
+    /**
+     * 获取多个商品的库存信息
+     * 
+     * @param array $itemIds 商品ID数组
+     * @return array 库存信息列表
+     */
+    public static function getItemsStock(array $itemIds): array
+    {
+        $warehouses = MexWarehouse::whereIn('item_id', $itemIds)->get();
+        
+        $result = [];
+        foreach ($warehouses as $warehouse) {
+            $result[$warehouse->item_id] = [
+                'item_id' => $warehouse->item_id,
+                'quantity' => $warehouse->quantity,
+                'total_buy_amount' => $warehouse->total_buy_amount,
+                'total_sell_amount' => $warehouse->total_sell_amount,
+                'total_buy_quantity' => $warehouse->total_buy_quantity,
+                'total_sell_quantity' => $warehouse->total_sell_quantity,
+                'average_buy_price' => self::calculateAveragePrice($warehouse->total_buy_amount, $warehouse->total_buy_quantity),
+                'average_sell_price' => self::calculateAveragePrice($warehouse->total_sell_amount, $warehouse->total_sell_quantity),
+                'last_transaction_at' => $warehouse->last_transaction_at,
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 检查库存是否充足
+     * 
+     * @param int $itemId 商品ID
+     * @param int $quantity 需要数量
+     * @return bool 是否充足
+     */
+    public static function checkStockSufficient(int $itemId, int $quantity): bool
+    {
+        $warehouse = MexWarehouse::where('item_id', $itemId)->first();
+        
+        if (!$warehouse) {
+            return false;
+        }
+
+        return $warehouse->quantity >= $quantity;
+    }
+
+    /**
+     * 获取所有有库存的商品
+     * 
+     * @return array 商品列表
+     */
+    public static function getAvailableItems(): array
+    {
+        $warehouses = MexWarehouse::where('quantity', '>', 0)
+            ->orderBy('quantity', 'desc')
+            ->get();
+
+        $result = [];
+        foreach ($warehouses as $warehouse) {
+            $result[] = [
+                'item_id' => $warehouse->item_id,
+                'quantity' => $warehouse->quantity,
+                'average_buy_price' => self::calculateAveragePrice($warehouse->total_buy_amount, $warehouse->total_buy_quantity),
+                'last_transaction_at' => $warehouse->last_transaction_at,
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取仓库统计信息
+     * 
+     * @return array 统计信息
+     */
+    public static function getWarehouseStats(): array
+    {
+        $stats = MexWarehouse::selectRaw('
+            COUNT(*) as total_items,
+            COUNT(CASE WHEN quantity > 0 THEN 1 END) as available_items,
+            SUM(quantity) as total_quantity,
+            SUM(total_buy_amount) as total_buy_amount,
+            SUM(total_sell_amount) as total_sell_amount,
+            SUM(total_buy_quantity) as total_buy_quantity,
+            SUM(total_sell_quantity) as total_sell_quantity
+        ')->first();
+
+        return [
+            'total_items' => $stats->total_items ?? 0,
+            'available_items' => $stats->available_items ?? 0,
+            'total_quantity' => $stats->total_quantity ?? 0,
+            'total_buy_amount' => $stats->total_buy_amount ?? '0.00000',
+            'total_sell_amount' => $stats->total_sell_amount ?? '0.00000',
+            'total_buy_quantity' => $stats->total_buy_quantity ?? 0,
+            'total_sell_quantity' => $stats->total_sell_quantity ?? 0,
+            'net_amount' => bcsub($stats->total_buy_amount ?? '0', $stats->total_sell_amount ?? '0', 5),
+            'net_quantity' => ($stats->total_buy_quantity ?? 0) - ($stats->total_sell_quantity ?? 0),
+        ];
+    }
+
+    /**
+     * 增加库存
+     * 
+     * @param int $itemId 商品ID
+     * @param int $quantity 数量
+     * @param string $amount 金额
+     * @return bool 操作结果
+     */
+    public static function addStock(int $itemId, int $quantity, string $amount): bool
+    {
+        try {
+            $warehouse = MexWarehouse::firstOrCreate(
+                ['item_id' => $itemId],
+                [
+                    'quantity' => 0,
+                    'total_buy_amount' => '0.00000',
+                    'total_sell_amount' => '0.00000',
+                    'total_buy_quantity' => 0,
+                    'total_sell_quantity' => 0,
+                ]
+            );
+
+            $warehouse->quantity += $quantity;
+            $warehouse->total_buy_quantity += $quantity;
+            $warehouse->total_buy_amount = bcadd($warehouse->total_buy_amount, $amount, 5);
+            $warehouse->last_transaction_at = now();
+            
+            return $warehouse->save();
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 减少库存
+     * 
+     * @param int $itemId 商品ID
+     * @param int $quantity 数量
+     * @param string $amount 金额
+     * @return bool 操作结果
+     */
+    public static function reduceStock(int $itemId, int $quantity, string $amount): bool
+    {
+        try {
+            $warehouse = MexWarehouse::where('item_id', $itemId)->first();
+            
+            if (!$warehouse || $warehouse->quantity < $quantity) {
+                return false;
+            }
+
+            $warehouse->quantity -= $quantity;
+            $warehouse->total_sell_quantity += $quantity;
+            $warehouse->total_sell_amount = bcadd($warehouse->total_sell_amount, $amount, 5);
+            $warehouse->last_transaction_at = now();
+            
+            return $warehouse->save();
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 计算平均价格
+     * 
+     * @param string $totalAmount 总金额
+     * @param int $totalQuantity 总数量
+     * @return string 平均价格
+     */
+    private static function calculateAveragePrice(string $totalAmount, int $totalQuantity): string
+    {
+        if ($totalQuantity <= 0) {
+            return '0.00000';
+        }
+        
+        return bcdiv($totalAmount, $totalQuantity, 5);
+    }
+}