WIN-2308041133\Administrator hace 1 día
padre
commit
1d61567cbd

+ 279 - 0
app/api/controller/Chat.php

@@ -0,0 +1,279 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\api\controller;
+
+use app\model\api\ChatRecord;
+use app\model\api\ChatUserRelation;
+use app\model\api\User;
+use app\services\chat\ChatBalanceService;
+use library\JwtAuth;
+use library\LoginSession;
+use think\Request;
+use think\Response;
+
+/**
+ * 聊天控制器
+ */
+class Chat extends JwtAuth
+{
+    protected $noNeedLogin = ['recordList'];
+    
+    /**
+     * 获取会话列表
+     * @return Response
+     */
+    public function sessionList(): Response
+    {
+        $sessions = ChatUserRelation::getUserSessions($this->userId);
+        
+        return $this->success('获取成功', [
+            'list' => $sessions,
+        ]);
+    }
+    
+    /**
+     * 初始化会话(首次聊天扣次数)
+     * @return Response
+     */
+    public function initSession(): Response
+    {
+        $data = $this->request->post();
+        $toUserId = (int)($data['to_user_id'] ?? 0);
+        
+        if ($toUserId <= 0) {
+            return $this->fail('对方用户ID错误');
+        }
+        
+        if ($toUserId == $this->userId) {
+            return $this->fail('不能和自己聊天');
+        }
+        
+        // 检查对方用户是否存在
+        $toUser = User::find($toUserId);
+        if (!$toUser) {
+            return $this->fail('用户不存在');
+        }
+        
+        // 创建或获取会话关系
+        $relation = ChatUserRelation::getOrCreate($this->userId, $toUserId);
+        
+        // 如果未扣过次数,则扣除
+        if ($relation && $relation->is_deduct == ChatUserRelation::NOT_DEDUCT) {
+            $result = ChatBalanceService::checkAndDeductFirstChat($this->userId, $toUserId);
+            
+            if (!$result['success']) {
+                return $this->fail($result['message']);
+            }
+        }
+        
+        $balance = ChatBalanceService::getBalance($this->userId);
+        
+        return $this->success('会话创建成功', [
+            'to_user_id' => $toUserId,
+            'to_nickname' => $toUser->nickname ?? '',
+            'to_avatar' => $toUser->avatar ?? '',
+            'balance' => $balance,
+        ]);
+    }
+    
+    /**
+     * 发送消息
+     * @return Response
+     */
+    public function send(): Response
+    {
+        $data = $this->request->post();
+        $toUserId = (int)($data['to_user_id'] ?? 0);
+        $content = trim($data['content'] ?? '');
+        $type = (int)($data['type'] ?? ChatRecord::TYPE_TEXT);
+        $formType = (int)($data['form_type'] ?? ChatRecord::FROM_PC);
+        
+        // 验证 form_type 有效值
+        if (!in_array($formType, [ChatRecord::FROM_PC, ChatRecord::FROM_WECHAT, ChatRecord::FROM_MINIAPP, ChatRecord::FROM_H5])) {
+            $formType = ChatRecord::FROM_PC;
+        }
+        
+        if ($toUserId <= 0) {
+            return $this->fail('对方用户ID错误');
+        }
+        
+        if (empty($content) && $type == ChatRecord::TYPE_TEXT) {
+            return $this->fail('消息内容不能为空');
+        }
+        
+        if ($toUserId == $this->userId) {
+            return $this->fail('不能和自己聊天');
+        }
+        
+        // 检查对方用户是否存在
+        $toUser = User::find($toUserId);
+        if (!$toUser) {
+            return $this->fail('用户不存在');
+        }
+        
+        // 检查会话关系是否存在,不存在则创建
+        $relation = ChatUserRelation::getOrCreate($this->userId, $toUserId);
+        
+        // 如果未扣过次数,则扣除
+        if ($relation && $relation->is_deduct == ChatUserRelation::NOT_DEDUCT) {
+            $result = ChatBalanceService::checkAndDeductFirstChat($this->userId, $toUserId);
+            
+            if (!$result['success']) {
+                return $this->fail($result['message']);
+            }
+        }
+        
+        // 保存聊天记录(包含来源类型)
+        $record = ChatRecord::saveRecord($this->userId, $toUserId, $content, $type, $formType);
+        
+        // 更新会话关系时间
+        ChatUserRelation::where('id', $relation->id)->update(['update_time' => time()]);
+        
+        // 广播消息给在线用户
+        self::broadcastMessage($toUserId, [
+            'type' => 'new_message',
+            'data' => [
+                'id' => $record->id,
+                'from_user_id' => $this->userId,
+                'to_user_id' => $toUserId,
+                'content' => $content,
+                'msg_type' => $type,
+                'form_type' => $formType,
+                'create_time' => $record->create_time,
+            ],
+        ]);
+        
+        return $this->success('发送成功', [
+            'record_id' => $record->id,
+            'create_time' => $record->create_time,
+        ]);
+    }
+    
+    /**
+     * 获取聊天记录
+     * @return Response
+     */
+    public function recordList(): Response
+    {
+        $data = $this->request->get();
+        $toUserId = (int)($data['to_user_id'] ?? 0);
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 20);
+        
+        if ($toUserId <= 0) {
+            return $this->fail('对方用户ID错误');
+        }
+        
+        $result = ChatRecord::getChatRecords($this->userId, $toUserId, $page, $limit);
+        
+        // 处理消息类型文本
+        foreach ($result['list'] as &$item) {
+            $item['type_text'] = ChatRecord::getTypeText($item['type']);
+            $item['form_type_text'] = ChatRecord::getFormTypeText($item['form_type'] ?? 0);
+            // 判断是否是自己发送的
+            $item['is_mine'] = $item['user_id'] == $this->userId;
+        }
+        
+        return $this->success('获取成功', $result);
+    }
+    
+    /**
+     * 标记消息已读
+     * @return Response
+     */
+    public function markRead(): Response
+    {
+        $data = $this->request->post();
+        $fromUserId = (int)($data['from_user_id'] ?? 0);
+        
+        if ($fromUserId <= 0) {
+            return $this->fail('用户ID错误');
+        }
+        
+        ChatRecord::markAsRead($this->userId, $fromUserId);
+        
+        return $this->success('标记成功');
+    }
+    
+    /**
+     * 获取未读消息数
+     * @return Response
+     */
+    public function unreadCount(): Response
+    {
+        $data = $this->request->get();
+        $fromUserId = $data['from_user_id'] ?? null;
+        
+        if ($fromUserId !== null) {
+            $count = ChatRecord::getUnreadCount($this->userId, (int)$fromUserId);
+        } else {
+            $count = ChatRecord::getTotalUnreadCount($this->userId);
+        }
+        
+        return $this->success('获取成功', [
+            'count' => $count,
+        ]);
+    }
+    
+    /**
+     * 删除会话
+     * @return Response
+     */
+    public function deleteSession(): Response
+    {
+        $data = $this->request->post();
+        $toUserId = (int)($data['to_user_id'] ?? 0);
+        
+        if ($toUserId <= 0) {
+            return $this->fail('对方用户ID错误');
+        }
+        
+        $result = ChatUserRelation::deleteSession($this->userId, $toUserId);
+        
+        if ($result) {
+            return $this->success('删除成功');
+        }
+        
+        return $this->fail('删除失败');
+    }
+    
+    /**
+     * 获取用户信息
+     * @return Response
+     */
+    public function getUserInfo(): Response
+    {
+        $data = $this->request->get();
+        $userId = (int)($data['user_id'] ?? 0);
+        
+        if ($userId <= 0) {
+            return $this->fail('用户ID错误');
+        }
+        
+        $user = User::find($userId);
+        if (!$user) {
+            return $this->fail('用户不存在');
+        }
+        
+        return $this->success('获取成功', [
+            'user_id' => $user->id,
+            'nickname' => $user->nickname ?? '',
+            'avatar' => $user->avatar ?? '',
+        ]);
+    }
+    
+    /**
+     * 广播消息给指定用户
+     * @param int $userId 用户ID
+     * @param array $data 消息数据
+     */
+    protected static function broadcastMessage(int $userId, array $data): void
+    {
+        try {
+            \Channel\Client::getInstance()->send('muyinjie_chat_' . $userId, $data);
+        } catch (\Throwable $e) {
+            // 忽略广播失败
+        }
+    }
+}

+ 138 - 0
app/api/controller/ChatBuy.php

@@ -0,0 +1,138 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\api\controller;
+
+use app\model\api\ChatBalanceLog;
+use app\model\api\ChatOrder;
+use app\services\chat\ChatBalanceService;
+use library\Helper;
+use library\JwtAuth;
+use think\Response;
+
+/**
+ * 聊天次数购买控制器
+ */
+class ChatBuy extends JwtAuth
+{
+    protected $noNeedLogin = [];
+    
+    /**
+     * 获取购买配置信息
+     * @return Response
+     */
+    public function config(): Response
+    {
+        $price = ChatBalanceService::getBuyPrice();
+        $balance = ChatBalanceService::getBalance($this->userId);
+        
+        return $this->success('获取成功', [
+            'price' => $price,           // 单次购买价格(积分)
+            'balance' => $balance,      // 当前剩余次数
+        ]);
+    }
+    
+    /**
+     * 创建购买订单
+     * @return Response
+     */
+    public function createOrder(): Response
+    {
+        $data = $this->request->post();
+        $chatNum = (int)($data['chat_num'] ?? 1);
+        
+        if ($chatNum <= 0) {
+            return $this->fail('购买数量必须大于0');
+        }
+        
+        $price = ChatBalanceService::getBuyPrice();
+        if ($price <= 0) {
+            return $this->fail('购买价格未设置');
+        }
+        
+        $totalPrice = $price * $chatNum;
+        
+        // 检查用户积分是否足够
+        $userScore = \app\model\api\UserScoreDetail::getUserScore($this->userId);
+        if ($userScore < $totalPrice) {
+            return $this->fail('积分不足,当前积分:' . $userScore);
+        }
+        
+        // 创建订单
+        $order = ChatOrder::createOrder($this->userId, $chatNum, $totalPrice);
+        
+        if (!$order) {
+            return $this->fail('订单创建失败');
+        }
+        
+        // 立即支付(积分支付,无需跳转)
+        $result = ChatBalanceService::buyWithScore($this->userId, $chatNum, $order);
+        
+        if (!$result) {
+            // 支付失败,取消订单
+            ChatOrder::cancelOrder($order->id);
+            return $this->fail('支付失败');
+        }
+        
+        // 更新订单状态
+        ChatOrder::paySuccess($order->id);
+        
+        $newBalance = ChatBalanceService::getBalance($this->userId);
+        
+        return $this->success('购买成功', [
+            'order_id' => $order->id,
+            'order_no' => $order->order_no,
+            'chat_num' => $chatNum,
+            'price' => $totalPrice,
+            'balance' => $newBalance,
+        ]);
+    }
+    
+    /**
+     * 获取当前聊天次数余额
+     * @return Response
+     */
+    public function balance(): Response
+    {
+        $balance = ChatBalanceService::getBalance($this->userId);
+        
+        return $this->success('获取成功', [
+            'balance' => $balance,
+        ]);
+    }
+    
+    /**
+     * 获取购买记录列表
+     * @return Response
+     */
+    public function orderList(): Response
+    {
+        $data = $this->request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 10);
+        
+        $result = ChatOrder::getUserOrders($this->userId, $page, $limit);
+        
+        // 处理订单状态文本
+        foreach ($result['list'] as &$item) {
+            $item['status_text'] = ChatOrder::getStatusText($item['status']);
+        }
+        
+        return $this->success('获取成功', $result);
+    }
+    
+    /**
+     * 获取变动明细列表
+     * @return Response
+     */
+    public function balanceLogList(): Response
+    {
+        $data = $this->request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 10);
+        
+        $result = ChatBalanceLog::getUserLogs($this->userId, $page, $limit);
+        
+        return $this->success('获取成功', $result);
+    }
+}

+ 26 - 0
app/api/route/chat.php

@@ -0,0 +1,26 @@
+<?php
+namespace app\api\route;
+
+use think\facade\Route;
+
+/**
+ * 聊天相关路由
+ */
+Route::group('chat', function () {
+    // 聊天次数购买
+    Route::get('config', 'ChatBuy/config');           // 获取购买配置
+    Route::post('createOrder', 'ChatBuy/createOrder'); // 创建购买订单
+    Route::get('balance', 'ChatBuy/balance');          // 获取余额
+    Route::get('orderList', 'ChatBuy/orderList');      // 购买记录
+    Route::get('balanceLogList', 'ChatBuy/balanceLogList'); // 变动明细
+    
+    // 聊天功能
+    Route::get('sessionList', 'Chat/sessionList');     // 会话列表
+    Route::post('initSession', 'Chat/initSession');    // 初始化会话
+    Route::post('send', 'Chat/send');                  // 发送消息
+    Route::get('recordList', 'Chat/recordList');        // 聊天记录
+    Route::post('markRead', 'Chat/markRead');          // 标记已读
+    Route::get('unreadCount', 'Chat/unreadCount');     // 未读数量
+    Route::post('deleteSession', 'Chat/deleteSession'); // 删除会话
+    Route::get('getUserInfo', 'Chat/getUserInfo');     // 获取用户信息
+})->middleware(['\\app\\api\\middleware\\AllowOriginMiddleware']);

+ 13 - 0
app/api/route/route.php

@@ -17,6 +17,19 @@ Route::group(function () {
     SeretKeyMiddleware::class
 ]);
 
+// 加载聊天相关路由
+require __DIR__ . '/chat.php';
+
+// 加载其他路由文件
+$routeDir = __DIR__;
+$routeFiles = ['user', 'cart', 'product', 'order', 'pay', 'shop', 'address', 'recharge', 'forum', 'education', 'pub'];
+foreach ($routeFiles as $file) {
+    $filePath = $routeDir . '/' . $file . '.php';
+    if (file_exists($filePath)) {
+        require $filePath;
+    }
+}
+
 
 /**
  * miss 路由

+ 99 - 0
app/model/api/ChatBalanceLog.php

@@ -0,0 +1,99 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model\api;
+
+use library\basic\BaseModel;
+use library\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 聊天次数变动明细模型
+ * @mixin \think\Model
+ */
+class ChatBalanceLog extends BaseModel
+{
+    use ModelTrait;
+    
+    protected $name = 'chat_balance_log';
+    
+    // 类型
+    const TYPE_GIFT = 1;           // 新用户赠送
+    const TYPE_BUY = 2;            // 积分购买
+    const TYPE_FIRST_CHAT = 3;     // 首次聊天消耗
+    const TYPE_ADMIN_ADD = 4;      // 管理员赠送
+    const TYPE_ADMIN_SUB = 5;     // 管理员扣除
+    
+    /**
+     * 记录变动
+     * @param int $userId 用户ID
+     * @param int $type 类型
+     * @param int $num 变动数量
+     * @param int $balance 变动后余额
+     * @param string $remark 备注
+     * @param int $relateId 关联ID
+     * @return self
+     */
+    public static function record(int $userId, int $type, int $num, int $balance, string $remark = '', int $relateId = 0): self
+    {
+        return self::create([
+            'user_id' => $userId,
+            'type' => $type,
+            'num' => $num,
+            'balance' => $balance,
+            'remark' => $remark,
+            'relate_id' => $relateId,
+            'create_time' => time(),
+        ]);
+    }
+    
+    /**
+     * 获取用户变动记录
+     * @param int $userId 用户ID
+     * @param int $page 页码
+     * @param int $limit 每页数量
+     * @return array
+     */
+    public static function getUserLogs(int $userId, int $page = 1, int $limit = 10): array
+    {
+        $data = self::where('user_id', $userId)
+            ->order('id desc')
+            ->page($page, $limit)
+            ->select();
+        
+        $total = self::where('user_id', $userId)->count();
+        
+        $list = [];
+        foreach ($data ?: [] as $item) {
+            $itemArr = $item->toArray();
+            $itemArr['type_text'] = self::getTypeText($item->type);
+            $itemArr['num_text'] = $item->num > 0 ? '+' . $item->num : $item->num;
+            $list[] = $itemArr;
+        }
+        
+        return [
+            'list' => $list,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ];
+    }
+    
+    /**
+     * 获取类型文本
+     * @param int $type 类型
+     * @return string
+     */
+    public static function getTypeText(int $type): string
+    {
+        $typeMap = [
+            self::TYPE_GIFT => '新用户赠送',
+            self::TYPE_BUY => '积分购买',
+            self::TYPE_FIRST_CHAT => '首次聊天消耗',
+            self::TYPE_ADMIN_ADD => '管理员赠送',
+            self::TYPE_ADMIN_SUB => '管理员扣除',
+        ];
+        
+        return $typeMap[$type] ?? '未知';
+    }
+}

+ 137 - 0
app/model/api/ChatOrder.php

@@ -0,0 +1,137 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model\api;
+
+use library\basic\BaseModel;
+use library\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 聊天次数购买订单模型
+ * @mixin \think\Model
+ */
+class ChatOrder extends BaseModel
+{
+    use ModelTrait;
+    
+    protected $name = 'chat_order';
+    
+    // 订单状态
+    const STATUS_PENDING = 0;   // 待支付
+    const STATUS_PAID = 1;      // 已支付
+    const STATUS_CANCEL = 2;    // 已取消
+    
+    /**
+     * 创建订单
+     * @param int $userId 用户ID
+     * @param int $chatNum 购买次数
+     * @param int $price 价格(积分)
+     * @return self
+     */
+    public static function createOrder(int $userId, int $chatNum, int $price): self
+    {
+        $orderNo = self::generateOrderNo();
+        
+        return self::create([
+            'order_no' => $orderNo,
+            'user_id' => $userId,
+            'chat_num' => $chatNum,
+            'price' => $price,
+            'status' => self::STATUS_PENDING,
+            'create_time' => time(),
+        ]);
+    }
+    
+    /**
+     * 生成订单号
+     * @return string
+     */
+    public static function generateOrderNo(): string
+    {
+        return 'C' . date('YmdHis') . rand(1000, 9999);
+    }
+    
+    /**
+     * 支付成功
+     * @param int $orderId 订单ID
+     * @return bool
+     */
+    public static function paySuccess(int $orderId): bool
+    {
+        return self::where('id', $orderId)
+            ->where('status', self::STATUS_PENDING)
+            ->update([
+                'status' => self::STATUS_PAID,
+                'pay_time' => time(),
+                'update_time' => time(),
+            ]) !== false;
+    }
+    
+    /**
+     * 取消订单
+     * @param int $orderId 订单ID
+     * @return bool
+     */
+    public static function cancelOrder(int $orderId): bool
+    {
+        return self::where('id', $orderId)
+            ->where('status', self::STATUS_PENDING)
+            ->update([
+                'status' => self::STATUS_CANCEL,
+                'update_time' => time(),
+            ]) !== false;
+    }
+    
+    /**
+     * 获取用户订单列表
+     * @param int $userId 用户ID
+     * @param int $page 页码
+     * @param int $limit 每页数量
+     * @return array
+     */
+    public static function getUserOrders(int $userId, int $page = 1, int $limit = 10): array
+    {
+        $where = ['user_id' => $userId];
+        
+        $data = self::where($where)
+            ->order('id desc')
+            ->page($page, $limit)
+            ->select();
+        
+        $total = self::where($where)->count();
+        
+        return [
+            'list' => $data ? $data->toArray() : [],
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ];
+    }
+    
+    /**
+     * 根据订单号查询
+     * @param string $orderNo 订单号
+     * @return self|null
+     */
+    public static function findByOrderNo(string $orderNo): ?self
+    {
+        return self::where('order_no', $orderNo)->find();
+    }
+    
+    /**
+     * 获取状态文本
+     * @param int $status 状态
+     * @return string
+     */
+    public static function getStatusText(int $status): string
+    {
+        $statusMap = [
+            self::STATUS_PENDING => '待支付',
+            self::STATUS_PAID => '已支付',
+            self::STATUS_CANCEL => '已取消',
+        ];
+        
+        return $statusMap[$status] ?? '未知';
+    }
+}

+ 172 - 0
app/model/api/ChatRecord.php

@@ -0,0 +1,172 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model\api;
+
+use library\basic\BaseModel;
+use library\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 聊天记录模型
+ * @mixin \think\Model
+ */
+class ChatRecord extends BaseModel
+{
+    use ModelTrait;
+    
+    protected $name = 'chat_record';
+    
+    // 消息类型
+    const TYPE_TEXT = 1;    // 文字
+    const TYPE_VOICE = 2;   // 语音
+    const TYPE_IMAGE = 3;   // 图片
+    
+    // 未读
+    const UNREAD = 0;
+    // 已读
+    const READ = 1;
+    
+    // 来源类型
+    const FROM_PC = 0;      // PC端
+    const FROM_WECHAT = 1;  // 微信
+    const FROM_MINIAPP = 2; // 小程序
+    const FROM_H5 = 3;      // H5
+    
+    /**
+     * 保存聊天记录
+     * @param int $userId 发送者ID
+     * @param int $toUserId 接收者ID
+     * @param string $content 消息内容
+     * @param int $type 消息类型
+     * @param int $formType 来源类型 0=PC, 1=微信, 2=小程序, 3=H5
+     * @return self
+     */
+    public static function saveRecord(int $userId, int $toUserId, string $content, int $type = self::TYPE_TEXT, int $formType = self::FROM_PC): self
+    {
+        return self::create([
+            'user_id' => $userId,
+            'to_user_id' => $toUserId,
+            'content' => $content,
+            'type' => $type,
+            'form_type' => $formType,
+            'is_read' => self::UNREAD,
+            'create_time' => time(),
+        ]);
+    }
+    
+    /**
+     * 获取与某用户的聊天记录
+     * @param int $userId 当前用户ID
+     * @param int $toUserId 对方用户ID
+     * @param int $page 页码
+     * @param int $limit 每页数量
+     * @return array
+     */
+    public static function getChatRecords(int $userId, int $toUserId, int $page = 1, int $limit = 20): array
+    {
+        $where = function ($query) use ($userId, $toUserId) {
+            $query->where(function ($q) use ($userId, $toUserId) {
+                $q->where('user_id', $userId)->where('to_user_id', $toUserId);
+            })->whereOr(function ($q) use ($userId, $toUserId) {
+                $q->where('user_id', $toUserId)->where('to_user_id', $userId);
+            });
+        };
+        
+        $data = self::where($where)
+            ->order('create_time desc')
+            ->page($page, $limit)
+            ->select();
+        
+        $total = self::where($where)->count();
+        
+        // 标记为已读
+        self::markAsRead($userId, $toUserId);
+        
+        return [
+            'list' => $data ? array_reverse($data->toArray()) : [],
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ];
+    }
+    
+    /**
+     * 标记消息为已读
+     * @param int $userId 当前用户ID
+     * @param int $fromUserId 发送者ID
+     * @return bool
+     */
+    public static function markAsRead(int $userId, int $fromUserId): bool
+    {
+        return self::where('user_id', $fromUserId)
+            ->where('to_user_id', $userId)
+            ->where('is_read', self::UNREAD)
+            ->update(['is_read' => self::READ]) !== false;
+    }
+    
+    /**
+     * 获取未读消息数
+     * @param int $userId 用户ID
+     * @param int|null $fromUserId 指定发送者(可选)
+     * @return int
+     */
+    public static function getUnreadCount(int $userId, ?int $fromUserId = null): int
+    {
+        $where = [
+            'to_user_id' => $userId,
+            'is_read' => self::UNREAD,
+        ];
+        
+        if ($fromUserId !== null) {
+            $where['user_id'] = $fromUserId;
+        }
+        
+        return self::where($where)->count();
+    }
+    
+    /**
+     * 获取所有未读消息数
+     * @param int $userId 用户ID
+     * @return int
+     */
+    public static function getTotalUnreadCount(int $userId): int
+    {
+        return self::where('to_user_id', $userId)
+            ->where('is_read', self::UNREAD)
+            ->count();
+    }
+    
+    /**
+     * 获取消息类型文本
+     * @param int $type 类型
+     * @return string
+     */
+    public static function getTypeText(int $type): string
+    {
+        $typeMap = [
+            self::TYPE_TEXT => '文字',
+            self::TYPE_VOICE => '语音',
+            self::TYPE_IMAGE => '图片',
+        ];
+        
+        return $typeMap[$type] ?? '未知';
+    }
+    
+    /**
+     * 获取来源类型文本
+     * @param int $formType 来源类型
+     * @return string
+     */
+    public static function getFormTypeText(int $formType): string
+    {
+        $formTypeMap = [
+            self::FROM_PC => 'PC端',
+            self::FROM_WECHAT => '微信',
+            self::FROM_MINIAPP => '小程序',
+            self::FROM_H5 => 'H5',
+        ];
+        
+        return $formTypeMap[$formType] ?? '未知';
+    }
+}

+ 176 - 0
app/model/api/ChatUserRelation.php

@@ -0,0 +1,176 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model\api;
+
+use library\basic\BaseModel;
+use library\traits\ModelTrait;
+use think\Model;
+
+/**
+ * 用户会话关系模型
+ * @mixin \think\Model
+ */
+class ChatUserRelation extends BaseModel
+{
+    use ModelTrait;
+    
+    protected $name = 'chat_user_relation';
+    
+    // 未扣次数
+    const NOT_DEDUCT = 0;
+    // 已扣次数
+    const IS_DEDUCT = 1;
+    
+    /**
+     * 获取或创建会话关系
+     * @param int $userId 用户ID
+     * @param int $toUserId 对方用户ID
+     * @return self|null
+     */
+    public static function getOrCreate(int $userId, int $toUserId): ?self
+    {
+        // 确保 userId < toUserId,保持一致性
+        if ($userId > $toUserId) {
+            [$userId, $toUserId] = [$toUserId, $userId];
+        }
+        
+        $relation = self::where('user_id', $userId)
+            ->where('to_user_id', $toUserId)
+            ->find();
+        
+        if (!$relation) {
+            $relation = self::create([
+                'user_id' => $userId,
+                'to_user_id' => $toUserId,
+                'is_deduct' => self::NOT_DEDUCT,
+                'create_time' => time(),
+                'update_time' => time(),
+            ]);
+        }
+        
+        return $relation;
+    }
+    
+    /**
+     * 检查是否已扣次数
+     * @param int $userId 用户ID
+     * @param int $toUserId 对方用户ID
+     * @return bool
+     */
+    public static function isDeducted(int $userId, int $toUserId): bool
+    {
+        // 确保 userId < toUserId
+        if ($userId > $toUserId) {
+            [$userId, $toUserId] = [$toUserId, $userId];
+        }
+        
+        $relation = self::where('user_id', $userId)
+            ->where('to_user_id', $toUserId)
+            ->find();
+        
+        return $relation && $relation->is_deduct == self::IS_DEDUCT;
+    }
+    
+    /**
+     * 标记已扣次数
+     * @param int $userId 用户ID
+     * @param int $toUserId 对方用户ID
+     * @return bool
+     */
+    public static function markDeducted(int $userId, int $toUserId): bool
+    {
+        // 确保 userId < toUserId
+        if ($userId > $toUserId) {
+            [$userId, $toUserId] = [$toUserId, $userId];
+        }
+        
+        return self::where('user_id', $userId)
+            ->where('to_user_id', $toUserId)
+            ->update([
+                'is_deduct' => self::IS_DEDUCT,
+                'update_time' => time(),
+            ]) !== false;
+    }
+    
+    /**
+     * 获取用户的所有会话列表
+     * @param int $userId 用户ID
+     * @return array
+     */
+    public static function getUserSessions(int $userId): array
+    {
+        $relations = self::where('user_id', $userId)
+            ->whereOr('to_user_id', $userId)
+            ->order('update_time desc')
+            ->select();
+        
+        $sessions = [];
+        foreach ($relations ?: [] as $relation) {
+            // 确定对方用户ID
+            $otherUserId = $relation->user_id == $userId ? $relation->to_user_id : $relation->user_id;
+            
+            // 获取对方用户信息
+            $user = User::find($otherUserId);
+            if (!$user) continue;
+            
+            // 获取最新一条聊天记录
+            $lastRecord = self::getLastRecord($userId, $otherUserId);
+            
+            // 获取未读消息数
+            $unreadCount = ChatRecord::getUnreadCount($userId, $otherUserId);
+            
+            $sessions[] = [
+                'id' => $relation->id,
+                'user_id' => $otherUserId,
+                'nickname' => $user->nickname ?? '',
+                'avatar' => $user->avatar ?? '',
+                'last_message' => $lastRecord['content'] ?? '',
+                'last_message_time' => $lastRecord['create_time'] ?? $relation->update_time,
+                'unread_count' => $unreadCount,
+                'is_deduct' => $relation->is_deduct,
+            ];
+        }
+        
+        return $sessions;
+    }
+    
+    /**
+     * 获取最新聊天记录
+     * @param int $userId 用户ID
+     * @param int $toUserId 对方用户ID
+     * @return array|null
+     */
+    protected static function getLastRecord(int $userId, int $toUserId): ?array
+    {
+        $record = ChatRecord::where(function ($query) use ($userId, $toUserId) {
+            $query->where(function ($q) use ($userId, $toUserId) {
+                $q->where('user_id', $userId)->where('to_user_id', $toUserId);
+            })->whereOr(function ($q) use ($userId, $toUserId) {
+                $q->where('user_id', $toUserId)->where('to_user_id', $userId);
+            });
+        })
+            ->order('create_time desc')
+            ->find();
+        
+        return $record ? $record->toArray() : null;
+    }
+    
+    /**
+     * 删除会话关系
+     * @param int $userId 用户ID
+     * @param int $toUserId 对方用户ID
+     * @return bool
+     */
+    public static function deleteSession(int $userId, int $toUserId): bool
+    {
+        // 确保 userId < toUserId
+        if ($userId > $toUserId) {
+            [$userId, $toUserId] = [$toUserId, $userId];
+        }
+        
+        return self::where('user_id', $userId)
+            ->where('to_user_id', $toUserId)
+            ->delete() !== false;
+    }
+}

+ 168 - 0
app/services/chat/ChatBalanceService.php

@@ -0,0 +1,168 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\services\chat;
+
+use app\model\api\ChatBalanceLog;
+use app\model\api\ChatOrder;
+use app\model\api\Sys;
+use app\model\api\User;
+use app\model\api\UserScoreDetail;
+
+/**
+ * 聊天次数余额服务
+ */
+class ChatBalanceService
+{
+    /**
+     * 获取用户聊天次数
+     * @param int $userId 用户ID
+     * @return int
+     */
+    public static function getBalance(int $userId): int
+    {
+        $user = User::find($userId);
+        return $user ? (int)($user->chat_num ?? 0) : 0;
+    }
+    
+    /**
+     * 增加聊天次数
+     * @param int $userId 用户ID
+     * @param int $num 增加数量
+     * @param int $type 类型
+     * @param string $remark 备注
+     * @param int $relateId 关联ID
+     * @return bool
+     */
+    public static function add(int $userId, int $num, int $type, string $remark = '', int $relateId = 0): bool
+    {
+        $user = User::find($userId);
+        if (!$user) {
+            return false;
+        }
+        
+        $newBalance = (int)$user->chat_num + $num;
+        
+        $user->chat_num = $newBalance;
+        $result = $user->save();
+        
+        if ($result) {
+            ChatBalanceLog::record($userId, $type, $num, $newBalance, $remark, $relateId);
+        }
+        
+        return $result;
+    }
+    
+    /**
+     * 减少聊天次数
+     * @param int $userId 用户ID
+     * @param int $num 减少数量
+     * @param int $type 类型
+     * @param string $remark 备注
+     * @param int $relateId 关联ID
+     * @return bool
+     */
+    public static function sub(int $userId, int $num, int $type, string $remark = '', int $relateId = 0): bool
+    {
+        $currentBalance = self::getBalance($userId);
+        
+        if ($currentBalance < $num) {
+            return false;
+        }
+        
+        $user = User::find($userId);
+        if (!$user) {
+            return false;
+        }
+        
+        $newBalance = $currentBalance - $num;
+        
+        $user->chat_num = $newBalance;
+        $result = $user->save();
+        
+        if ($result) {
+            ChatBalanceLog::record($userId, $type, -$num, $newBalance, $remark, $relateId);
+        }
+        
+        return $result;
+    }
+    
+    /**
+     * 扣除积分增加聊天次数(购买流程)
+     * @param int $userId 用户ID
+     * @param int $chatNum 购买次数
+     * @param ChatOrder $order 订单
+     * @return bool
+     */
+    public static function buyWithScore(int $userId, int $chatNum, ChatOrder $order): bool
+    {
+        $price = (int)$order->price;
+        
+        // 检查用户积分是否足够
+        $userScore = UserScoreDetail::getUserScore($userId);
+        if ($userScore < $price) {
+            return false;
+        }
+        
+        // 扣除积分
+        $scoreResult = UserScoreDetail::scoreChange($userId, -$price, '购买聊天次数', $order->id);
+        if (!$scoreResult) {
+            return false;
+        }
+        
+        // 增加聊天次数
+        return self::add($userId, $chatNum, ChatBalanceLog::TYPE_BUY, '积分购买聊天次数', $order->id);
+    }
+    
+    /**
+     * 新用户赠送聊天次数
+     * @param int $userId 用户ID
+     * @param int $num 赠送次数
+     * @return bool
+     */
+    public static function giftForNewUser(int $userId, int $num): bool
+    {
+        return self::add($userId, $num, ChatBalanceLog::TYPE_GIFT, '新用户赠送');
+    }
+    
+    /**
+     * 检查并扣除首次聊天次数
+     * @param int $userId 用户ID(发起聊天方)
+     * @param int $toUserId 对方用户ID
+     * @return array ['success' => bool, 'message' => string]
+     */
+    public static function checkAndDeductFirstChat(int $userId, int $toUserId): array
+    {
+        // 检查是否已经扣过次数
+        if (\app\model\api\ChatUserRelation::isDeducted($userId, $toUserId)) {
+            return ['success' => true, 'message' => '已扣过次数'];
+        }
+        
+        // 检查余额是否足够
+        $balance = self::getBalance($userId);
+        if ($balance <= 0) {
+            return ['success' => false, 'message' => '聊天次数不足'];
+        }
+        
+        // 扣除次数
+        $result = self::sub($userId, 1, ChatBalanceLog::TYPE_FIRST_CHAT, '首次与用户' . $toUserId . '聊天', $toUserId);
+        
+        if ($result) {
+            // 标记为已扣次数
+            \app\model\api\ChatUserRelation::markDeducted($userId, $toUserId);
+            return ['success' => true, 'message' => '扣除成功'];
+        }
+        
+        return ['success' => false, 'message' => '扣除失败'];
+    }
+    
+    /**
+     * 获取购买价格
+     * @return int 单次购买价格(积分)
+     */
+    public static function getBuyPrice(): int
+    {
+        $sys = Sys::find(1);
+        return $sys ? (int)($sys->chat_price ?? 0) : 0;
+    }
+}

+ 1 - 1
app/services/workerman/chat/ChatHandle.php

@@ -73,7 +73,7 @@ class ChatHandle
     {
         $to_uid = $res['data']['to_uid'] ?? 0;
         $msn = $res['data']['msn'] ?? '';
-        $msn_type = $res['data']['type'] ?? 1; // 1=文本, 2=图片, 3=语音
+        $msn_type = $res['data']['type'] ?? 1; // 1=文字, 2=语音, 3=图片
 
         if (!$to_uid) {
             return $response->send('err_tip', ['msg' => '用户不存在']);

+ 465 - 0
app/system/controller/Chat.php

@@ -0,0 +1,465 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\system\controller;
+
+use app\model\api\ChatBalanceLog;
+use app\model\api\ChatOrder;
+use app\model\api\ChatRecord;
+use app\model\api\ChatUserRelation;
+use app\model\api\User;
+use app\services\chat\ChatBalanceService;
+use library\basic\BaseController;
+use think\Request;
+use think\Response;
+
+/**
+ * 聊天管理控制器
+ */
+class Chat extends BaseController
+{
+    /**
+     * 获取购买订单列表
+     * @param Request $request
+     * @return Response
+     */
+    public function orderList(Request $request): Response
+    {
+        $data = $request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 15);
+        $status = isset($data['status']) ? (int)$data['status'] : null;
+        $orderNo = trim($data['order_no'] ?? '');
+        $userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
+        
+        $where = [];
+        if ($status !== null) {
+            $where[] = ['status', '=', $status];
+        }
+        if (!empty($orderNo)) {
+            $where[] = ['order_no', '=', $orderNo];
+        }
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        
+        $query = ChatOrder::where($where);
+        $total = $query->count();
+        $list = $query->order('id desc')
+            ->page($page, $limit)
+            ->select();
+        
+        // 处理数据
+        $dataList = [];
+        foreach ($list ?: [] as $item) {
+            $itemArr = $item->toArray();
+            // 获取用户信息
+            $user = User::find($item->user_id);
+            $itemArr['user_nickname'] = $user ? ($user->nickname ?? '') : '';
+            $itemArr['user_phone'] = $user ? ($user->phone ?? '') : '';
+            $itemArr['status_text'] = ChatOrder::getStatusText($item->status);
+            $itemArr['create_time_text'] = date('Y-m-d H:i:s', $item->create_time);
+            $itemArr['pay_time_text'] = $item->pay_time ? date('Y-m-d H:i:s', $item->pay_time) : '-';
+            $dataList[] = $itemArr;
+        }
+        
+        return $this->success('获取成功', [
+            'list' => $dataList,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ]);
+    }
+    
+    /**
+     * 获取聊天次数变动明细列表
+     * @param Request $request
+     * @return Response
+     */
+    public function balanceLogList(Request $request): Response
+    {
+        $data = $request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 15);
+        $type = isset($data['type']) ? (int)$data['type'] : null;
+        $userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
+        
+        $where = [];
+        if ($type !== null) {
+            $where[] = ['type', '=', $type];
+        }
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        
+        $query = ChatBalanceLog::where($where);
+        $total = $query->count();
+        $list = $query->order('id desc')
+            ->page($page, $limit)
+            ->select();
+        
+        // 处理数据
+        $dataList = [];
+        foreach ($list ?: [] as $item) {
+            $itemArr = $item->toArray();
+            // 获取用户信息
+            $user = User::find($item->user_id);
+            $itemArr['user_nickname'] = $user ? ($user->nickname ?? '') : '';
+            $itemArr['user_phone'] = $user ? ($user->phone ?? '') : '';
+            $itemArr['type_text'] = ChatBalanceLog::getTypeText($item->type);
+            $itemArr['num_text'] = $item->num > 0 ? '+' . $item->num : $item->num;
+            $itemArr['create_time_text'] = date('Y-m-d H:i:s', $item->create_time);
+            $dataList[] = $itemArr;
+        }
+        
+        return $this->success('获取成功', [
+            'list' => $dataList,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ]);
+    }
+    
+    /**
+     * 获取聊天记录列表
+     * @param Request $request
+     * @return Response
+     */
+    public function recordList(Request $request): Response
+    {
+        $data = $request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 15);
+        $userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
+        $toUserId = isset($data['to_user_id']) ? (int)$data['to_user_id'] : 0;
+        
+        $where = [];
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        if ($toUserId > 0) {
+            $where[] = ['to_user_id', '=', $toUserId];
+        }
+        
+        $query = ChatRecord::where($where);
+        $total = $query->count();
+        $list = $query->order('id desc')
+            ->page($page, $limit)
+            ->select();
+        
+        // 处理数据
+        $dataList = [];
+        foreach ($list ?: [] as $item) {
+            $itemArr = $item->toArray();
+            // 获取发送者信息
+            $user = User::find($item->user_id);
+            $itemArr['user_nickname'] = $user ? ($user->nickname ?? '') : '';
+            // 获取接收者信息
+            $toUser = User::find($item->to_user_id);
+            $itemArr['to_user_nickname'] = $toUser ? ($toUser->nickname ?? '') : '';
+            $itemArr['type_text'] = ChatRecord::getTypeText($item->type);
+            $itemArr['is_read_text'] = $item->is_read == 1 ? '已读' : '未读';
+            $itemArr['create_time_text'] = date('Y-m-d H:i:s', $item->create_time);
+            $dataList[] = $itemArr;
+        }
+        
+        return $this->success('获取成功', [
+            'list' => $dataList,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ]);
+    }
+    
+    /**
+     * 获取用户聊天会话列表
+     * @param Request $request
+     * @return Response
+     */
+    public function sessionList(Request $request): Response
+    {
+        $data = $request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 15);
+        $userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
+        
+        if ($userId <= 0) {
+            return $this->fail('用户ID不能为空');
+        }
+        
+        $relations = ChatUserRelation::where('user_id', $userId)
+            ->whereOr('to_user_id', $userId)
+            ->order('update_time desc')
+            ->page($page, $limit)
+            ->select();
+        
+        $total = ChatUserRelation::where('user_id', $userId)
+            ->whereOr('to_user_id', $userId)
+            ->count();
+        
+        $dataList = [];
+        foreach ($relations ?: [] as $relation) {
+            $otherUserId = $relation->user_id == $userId ? $relation->to_user_id : $relation->user_id;
+            $user = User::find($otherUserId);
+            
+            $dataList[] = [
+                'id' => $relation->id,
+                'user_id' => $otherUserId,
+                'nickname' => $user ? ($user->nickname ?? '') : '',
+                'phone' => $user ? ($user->phone ?? '') : '',
+                'is_deduct' => $relation->is_deduct,
+                'is_deduct_text' => $relation->is_deduct == 1 ? '已扣次数' : '未扣次数',
+                'create_time' => date('Y-m-d H:i:s', $relation->create_time),
+                'update_time' => date('Y-m-d H:i:s', $relation->update_time),
+            ];
+        }
+        
+        return $this->success('获取成功', [
+            'list' => $dataList,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ]);
+    }
+    
+    /**
+     * 获取用户聊天次数余额列表
+     * @param Request $request
+     * @return Response
+     */
+    public function userBalanceList(Request $request): Response
+    {
+        $data = $request->get();
+        $page = (int)($data['page'] ?? 1);
+        $limit = (int)($data['limit'] ?? 15);
+        $keyword = trim($data['keyword'] ?? '');
+        
+        $query = User::where('chat_num', '>', 0);
+        
+        if (!empty($keyword)) {
+            $query->where(function ($q) use ($keyword) {
+                $q->whereOr([
+                    ['nickname', 'like', '%' . $keyword . '%'],
+                    ['phone', 'like', '%' . $keyword . '%'],
+                ]);
+            });
+        }
+        
+        $total = $query->count();
+        $list = $query->order('chat_num desc')
+            ->page($page, $limit)
+            ->select();
+        
+        $dataList = [];
+        foreach ($list ?: [] as $item) {
+            $dataList[] = [
+                'id' => $item->id,
+                'user_id' => $item->id,
+                'nickname' => $item->nickname ?? '',
+                'phone' => $item->phone ?? '',
+                'chat_num' => $item->chat_num ?? 0,
+                'avatar' => $item->avatar ?? '',
+            ];
+        }
+        
+        return $this->success('获取成功', [
+            'list' => $dataList,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ]);
+    }
+    
+    /**
+     * 管理员给用户添加聊天次数
+     * @param Request $request
+     * @return Response
+     */
+    public function addBalance(Request $request): Response
+    {
+        $data = $request->post();
+        $userId = (int)($data['user_id'] ?? 0);
+        $num = (int)($data['num'] ?? 0);
+        $remark = trim($data['remark'] ?? '管理员操作');
+        
+        if ($userId <= 0) {
+            return $this->fail('用户ID错误');
+        }
+        
+        if ($num <= 0) {
+            return $this->fail('添加数量必须大于0');
+        }
+        
+        $user = User::find($userId);
+        if (!$user) {
+            return $this->fail('用户不存在');
+        }
+        
+        $result = ChatBalanceService::add($userId, $num, ChatBalanceLog::TYPE_ADMIN_ADD, $remark);
+        
+        if ($result) {
+            return $this->success('添加成功');
+        }
+        
+        return $this->fail('添加失败');
+    }
+    
+    /**
+     * 管理员扣除用户聊天次数
+     * @param Request $request
+     * @return Response
+     */
+    public function subBalance(Request $request): Response
+    {
+        $data = $request->post();
+        $userId = (int)($data['user_id'] ?? 0);
+        $num = (int)($data['num'] ?? 0);
+        $remark = trim($data['remark'] ?? '管理员操作');
+        
+        if ($userId <= 0) {
+            return $this->fail('用户ID错误');
+        }
+        
+        if ($num <= 0) {
+            return $this->fail('扣除数量必须大于0');
+        }
+        
+        $user = User::find($userId);
+        if (!$user) {
+            return $this->fail('用户不存在');
+        }
+        
+        $currentBalance = (int)($user->chat_num ?? 0);
+        if ($currentBalance < $num) {
+            return $this->fail('用户聊天次数不足,当前余额:' . $currentBalance);
+        }
+        
+        $result = ChatBalanceService::sub($userId, $num, ChatBalanceLog::TYPE_ADMIN_SUB, $remark);
+        
+        if ($result) {
+            return $this->success('扣除成功');
+        }
+        
+        return $this->fail('扣除失败');
+    }
+    
+    /**
+     * 获取聊天统计数据
+     * @return Response
+     */
+    public function statistics(): Response
+    {
+        // 总订单数
+        $totalOrders = ChatOrder::count();
+        // 已支付订单数
+        $paidOrders = ChatOrder::where('status', ChatOrder::STATUS_PAID)->count();
+        // 总聊天次数消耗
+        $totalConsume = ChatBalanceLog::where('type', ChatBalanceLog::TYPE_FIRST_CHAT)->sum('num');
+        // 购买次数总额
+        $totalBuy = ChatBalanceLog::where('type', ChatBalanceLog::TYPE_BUY)->sum('num');
+        // 有聊天次数的用户数
+        $usersWithBalance = User::where('chat_num', '>', 0)->count();
+        // 总会话数
+        $totalSessions = ChatUserRelation::count();
+        
+        return $this->success('获取成功', [
+            'total_orders' => $totalOrders,
+            'paid_orders' => $paidOrders,
+            'total_consume' => abs((int)$totalConsume),
+            'total_buy' => (int)$totalBuy,
+            'users_with_balance' => $usersWithBalance,
+            'total_sessions' => $totalSessions,
+        ]);
+    }
+    
+    /**
+     * 删除聊天会话记录
+     * @param Request $request
+     * @return Response
+     */
+    public function deleteSession(Request $request): Response
+    {
+        $data = $request->post();
+        $relationId = (int)($data['id'] ?? 0);
+        
+        if ($relationId <= 0) {
+            return $this->fail('会话ID错误');
+        }
+        
+        $relation = ChatUserRelation::find($relationId);
+        if (!$relation) {
+            return $this->fail('会话不存在');
+        }
+        
+        // 删除会话关系
+        $relation->delete();
+        
+        return $this->success('删除成功');
+    }
+    
+    /**
+     * 删除聊天记录
+     * @param Request $request
+     * @return Response
+     */
+    public function deleteRecord(Request $request): Response
+    {
+        $data = $request->post();
+        $id = (int)($data['id'] ?? 0);
+        
+        if ($id <= 0) {
+            return $this->fail('记录ID错误');
+        }
+        
+        $record = ChatRecord::find($id);
+        if (!$record) {
+            return $this->fail('记录不存在');
+        }
+        
+        $record->delete();
+        
+        return $this->success('删除成功');
+    }
+    
+    /**
+     * 设置聊天价格配置
+     * @param Request $request
+     * @return Response
+     */
+    public function setConfig(Request $request): Response
+    {
+        $data = $request->post();
+        $chatPrice = (int)($data['chat_price'] ?? 0);
+        
+        if ($chatPrice <= 0) {
+            return $this->fail('价格必须大于0');
+        }
+        
+        $sys = \app\model\api\Sys::find(1);
+        if (!$sys) {
+            return $this->fail('系统配置不存在');
+        }
+        
+        $sys->chat_price = $chatPrice;
+        
+        if ($sys->save()) {
+            return $this->success('设置成功');
+        }
+        
+        return $this->fail('设置失败');
+    }
+    
+    /**
+     * 获取聊天价格配置
+     * @return Response
+     */
+    public function getConfig(): Response
+    {
+        $sys = \app\model\api\Sys::find(1);
+        $price = $sys ? (int)($sys->chat_price ?? 0) : 0;
+        
+        return $this->success('获取成功', [
+            'chat_price' => $price,
+        ]);
+    }
+}

+ 45 - 0
app/system/route/chat.php

@@ -0,0 +1,45 @@
+<?php
+namespace app\system\route;
+
+use think\facade\Route;
+
+/**
+ * 聊天管理路由
+ */
+Route::group('chat', function () {
+    // 统计数据
+    Route::get('statistics', 'Chat/statistics');
+    
+    // 购买订单管理
+    Route::get('orderList', 'Chat/orderList');
+    
+    // 聊天次数变动明细
+    Route::get('balanceLogList', 'Chat/balanceLogList');
+    
+    // 聊天记录管理
+    Route::get('recordList', 'Chat/recordList');
+    
+    // 用户会话列表
+    Route::get('sessionList', 'Chat/sessionList');
+    
+    // 用户余额列表
+    Route::get('userBalanceList', 'Chat/userBalanceList');
+    
+    // 添加用户聊天次数
+    Route::post('addBalance', 'Chat/addBalance');
+    
+    // 扣除用户聊天次数
+    Route::post('subBalance', 'Chat/subBalance');
+    
+    // 删除会话
+    Route::post('deleteSession', 'Chat/deleteSession');
+    
+    // 删除聊天记录
+    Route::post('deleteRecord', 'Chat/deleteRecord');
+    
+    // 设置聊天价格
+    Route::post('setConfig', 'Chat/setConfig');
+    
+    // 获取聊天价格
+    Route::get('getConfig', 'Chat/getConfig');
+});

+ 3 - 0
app/system/route/route.php

@@ -33,6 +33,9 @@ Route::group(function () {
 
 })->middleware(AllowOriginMiddleware::class);
 
+// 加载聊天管理路由
+require __DIR__ . '/chat.php';
+
 /**
  * miss 路由
  */

+ 76 - 0
socket.php

@@ -0,0 +1,76 @@
+<?php
+/**
+ * WebSocket 服务端启动文件
+ * 
+ * 使用方法:
+ *   php socket.php start          # 调试模式启动(前台运行)
+ *   php socket.php start -d       # 守护进程模式启动(后台运行)
+ *   php socket.php stop           # 停止服务
+ *   php socket.php restart        # 重启服务
+ *   php socket.php status         # 查看运行状态
+ */
+
+require __DIR__ . '/vendor/autoload.php';
+
+use Workerman\Worker;
+use Workerman\Connection\TcpConnection;
+
+// 日志目录
+define('LOG_PATH', __DIR__ . '/runtime/logs/workerman/');
+if (!is_dir(LOG_PATH)) {
+    mkdir(LOG_PATH, 0755, true);
+}
+
+// 创建 WebSocket 服务
+$ws_worker = new Worker('websocket://0.0.0.0:2345');
+
+// 设置进程名称
+$ws_worker->name = 'ChatWebSocket';
+
+// 设置进程数
+$ws_worker->count = 4;
+
+// 设置心跳(60秒无响应则断开)
+$ws_worker->pingInterval = 60;
+$ws_worker->pingNotResponseLimit = 1;
+
+// 当进程启动时
+$ws_worker->onWorkerStart = function($worker) {
+    echo "[Worker #{$worker->id}] started\n";
+    
+    // 初始化 ChatService
+    $chatService = new \app\services\workerman\chat\ChatService($worker);
+    $worker->chatService = $chatService;
+};
+
+// 当客户端连接时
+$ws_worker->onConnect = function(TcpConnection $connection) {
+    $addr = $connection->getRemoteAddress();
+    echo "[Connect] {$addr}\n";
+};
+
+// 当收到客户端消息时
+$ws_worker->onMessage = function(TcpConnection $connection, $data) use (&$chatService) {
+    $chatService->onMessage($connection, $data);
+};
+
+// 当客户端断开连接时
+$ws_worker->onClose = function(TcpConnection $connection) use (&$chatService) {
+    $chatService->onClose($connection);
+};
+
+// 当连接出错时
+$ws_worker->onError = function(TcpConnection $connection, $code, $msg) {
+    echo "[Error] {$code}: {$msg}\n";
+};
+
+// 输出启动信息
+echo "\n";
+echo "========================================\n";
+echo "  Chat WebSocket Server\n";
+echo "  Address: ws://0.0.0.0:2345\n";
+echo "  PID: " . getmypid() . "\n";
+echo "========================================\n\n";
+
+// 运行
+Worker::runAll();