From d1c7003d18c652eeab4eb33c7bd51dfe8f7469c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=A3=9E=E6=B4=8B?=
<15778543+xufeiyang6017@user.noreply.gitee.com>
Date: Wed, 17 Dec 2025 10:33:02 +0800
Subject: [PATCH 1/4] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?=
=?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E6=B8=85=E7=90=86=E9=A1=B9=E7=9B=AE?=
=?UTF-8?q?=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
how origin | 14 -
page.html | 480 --------------
simple_chat_test.js | 138 ----
test-customer-service.js | 333 ----------
test_chat_connection.js | 96 ---
test_chat_functionality.js | 1276 ------------------------------------
update_product_table.js | 75 ---
7 files changed, 2412 deletions(-)
delete mode 100644 how origin
delete mode 100644 page.html
delete mode 100644 simple_chat_test.js
delete mode 100644 test-customer-service.js
delete mode 100644 test_chat_connection.js
delete mode 100644 test_chat_functionality.js
delete mode 100644 update_product_table.js
diff --git a/how origin b/how origin
deleted file mode 100644
index 1585038..0000000
--- a/how origin
+++ /dev/null
@@ -1,14 +0,0 @@
-
- SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
-
- Commands marked with * may be preceded by a number, _N.
- Notes in parentheses indicate the behavior if _N is given.
- A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
-
- h H Display this help.
- q :q Q :Q ZZ Exit.
- ---------------------------------------------------------------------------
-
- MMOOVVIINNGG
-
- e ^E j ^N CR * Forward one line (or _N lines).
diff --git a/page.html b/page.html
deleted file mode 100644
index db31592..0000000
--- a/page.html
+++ /dev/null
@@ -1,480 +0,0 @@
-
-
-
-
-
-
- å½å
æ°é»_æ°é»ä¸å¿_æ°æµªç½
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ä¸è½½æ°æµªæ°é»
-
-
-
-
- æ°æµªæ°é»App
-
-

-
-
-
- æè§åé¦
-
-
-
-
- è¿åé¡¶é¨
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/simple_chat_test.js b/simple_chat_test.js
deleted file mode 100644
index b57e76f..0000000
--- a/simple_chat_test.js
+++ /dev/null
@@ -1,138 +0,0 @@
-// 简化版聊天功能测试
-
-// 服务器配置
-const SERVER_URL = 'ws://localhost:3003';
-
-// 测试数据
-const managerData = {
- userId: 'manager_001',
- type: 'manager',
- name: '客服小刘'
-};
-
-const userData = {
- userId: 'user_001',
- type: 'user',
- name: '测试用户'
-};
-
-// 测试结果跟踪
-const testResults = {
- managerConnection: false,
- managerAuth: false,
- userConnection: false,
- userAuth: false,
- messageExchange: false,
- onlineStatusDetection: false,
- messageCenterFunctionality: false
-};
-
-function runSimpleChatTest() {
- console.log('=== 开始简化版聊天功能测试 ===');
-
- // 模拟客服连接
- try {
- const WebSocket = require('ws');
- const managerSocket = new WebSocket(SERVER_URL);
-
- managerSocket.on('open', () => {
- console.log('[✅] 客服连接已建立');
- testResults.managerConnection = true;
-
- // 发送客服认证
- const authMessage = {
- type: 'auth',
- data: {
- userId: managerData.userId,
- type: managerData.type,
- name: managerData.name
- }
- };
- console.log('发送客服认证:', authMessage);
- managerSocket.send(JSON.stringify(authMessage));
- });
-
- managerSocket.on('message', (data) => {
- console.log('[客服收到消息]:', data.toString());
- const message = JSON.parse(data);
-
- // 检查认证结果
- if (message.type === 'auth_success' || message.action === 'auth_response') {
- console.log('[✅] 客服认证成功');
- testResults.managerAuth = true;
- }
- });
-
- managerSocket.on('error', (error) => {
- console.error('[❌] 客服连接错误:', error.message);
- });
-
- managerSocket.on('close', () => {
- console.log('[🔌] 客服连接已关闭');
- });
-
- // 延迟创建用户连接
- setTimeout(() => {
- const userSocket = new WebSocket(SERVER_URL);
-
- userSocket.on('open', () => {
- console.log('[✅] 用户连接已建立');
- testResults.userConnection = true;
-
- // 发送用户认证
- const userAuth = {
- type: 'auth',
- data: {
- userId: userData.userId,
- type: userData.type,
- name: userData.name
- }
- };
- console.log('发送用户认证:', userAuth);
- userSocket.send(JSON.stringify(userAuth));
- });
-
- userSocket.on('message', (data) => {
- console.log('[用户收到消息]:', data.toString());
- });
-
- // 5秒后发送测试消息
- setTimeout(() => {
- if (userSocket.readyState === WebSocket.OPEN) {
- const testMessage = {
- type: 'chat',
- from: userData.userId,
- to: managerData.userId,
- content: '你好,这是一条测试消息',
- timestamp: Date.now()
- };
- console.log('用户发送测试消息:', testMessage);
- userSocket.send(JSON.stringify(testMessage));
- }
- }, 5000);
-
- }, 3000);
-
- // 15秒后显示测试结果
- setTimeout(() => {
- console.log('\n=== 测试结果 ===');
- console.log('客服连接:', testResults.managerConnection ? '✅ 成功' : '❌ 失败');
- console.log('客服认证:', testResults.managerAuth ? '✅ 成功' : '❌ 失败');
- console.log('用户连接:', testResults.userConnection ? '✅ 成功' : '❌ 失败');
- console.log('\n测试完成!');
-
- // 关闭连接
- managerSocket.close();
- process.exit(0);
-
- }, 15000);
-
- } catch (error) {
- console.error('测试运行失败:', error.message);
- }
-}
-
-// 运行测试
-if (require.main === module) {
- runSimpleChatTest();
-}
diff --git a/test-customer-service.js b/test-customer-service.js
deleted file mode 100644
index d2a47d1..0000000
--- a/test-customer-service.js
+++ /dev/null
@@ -1,333 +0,0 @@
-// 客服功能测试脚本
-// 用于验证客服认证、身份判断和双向沟通功能
-
-console.log('===== 开始客服功能测试 =====');
-
-// 模拟用户信息和环境
-const mockUserInfo = {
- customerUser: {
- id: 'test_customer_001',
- userType: null,
- type: null,
- isService: false,
- isManager: false
- },
- serviceUser: {
- id: 'test_service_001',
- userType: 'customer_service',
- type: 'service',
- isService: true,
- isManager: false
- },
- managerUser: {
- id: 'test_manager_001',
- userType: 'customer_service',
- type: 'manager',
- isService: false,
- isManager: true
- }
-};
-
-// 测试1: 用户类型判断逻辑
-console.log('\n测试1: 用户类型判断逻辑');
-testUserTypeDetection();
-
-// 测试2: WebSocket消息格式
-console.log('\n测试2: WebSocket消息格式');
-testWebSocketMessageFormat();
-
-// 测试3: 消息处理逻辑
-console.log('\n测试3: 消息处理逻辑');
-testMessageProcessing();
-
-// 测试4: 双向通信模式
-console.log('\n测试4: 双向通信模式');
-testBidirectionalCommunication();
-
-console.log('\n===== 测试完成 =====');
-
-// 测试用户类型判断逻辑
-function testUserTypeDetection() {
- console.log('- 测试用户类型判断函数');
-
- // 模拟用户类型判断函数
- function detectUserType(userInfo) {
- if (!userInfo) return 'customer';
-
- if (userInfo.userType === 'customer_service' ||
- userInfo.type === 'service' ||
- userInfo.type === 'manager' ||
- userInfo.isService ||
- userInfo.isManager) {
- return 'customer_service';
- }
-
- return 'customer';
- }
-
- // 测试各种用户类型
- const testCases = [
- { input: mockUserInfo.customerUser, expected: 'customer', desc: '普通用户' },
- { input: mockUserInfo.serviceUser, expected: 'customer_service', desc: '客服用户' },
- { input: mockUserInfo.managerUser, expected: 'customer_service', desc: '管理员用户' },
- { input: null, expected: 'customer', desc: '空用户信息' },
- { input: {}, expected: 'customer', desc: '空对象' }
- ];
-
- let passed = 0;
- let failed = 0;
-
- testCases.forEach((testCase, index) => {
- const result = detectUserType(testCase.input);
- const isPass = result === testCase.expected;
-
- if (isPass) {
- passed++;
- console.log(` ✓ 测试${index + 1} (${testCase.desc}): 期望 ${testCase.expected}, 结果 ${result}`);
- } else {
- failed++;
- console.log(` ✗ 测试${index + 1} (${testCase.desc}): 期望 ${testCase.expected}, 结果 ${result}`);
- }
- });
-
- console.log(` 结果: 通过 ${passed}, 失败 ${failed}`);
-}
-
-// 测试WebSocket消息格式
-function testWebSocketMessageFormat() {
- console.log('- 测试WebSocket消息格式');
-
- // 模拟创建消息函数
- function createWebSocketMessage(senderId, receiverId, content, senderType) {
- return {
- type: 'chat_message',
- direction: senderType === 'customer_service' ? 'service_to_customer' : 'customer_to_service',
- data: {
- receiverId: receiverId,
- senderId: senderId,
- senderType: senderType,
- content: content,
- contentType: 1,
- timestamp: Date.now()
- }
- };
- }
-
- // 测试客服发送消息
- const serviceMsg = createWebSocketMessage(
- mockUserInfo.serviceUser.id,
- mockUserInfo.customerUser.id,
- '您好,有什么可以帮助您的吗?',
- 'customer_service'
- );
-
- // 测试客户发送消息
- const customerMsg = createWebSocketMessage(
- mockUserInfo.customerUser.id,
- mockUserInfo.serviceUser.id,
- '我想咨询一下产品信息',
- 'customer'
- );
-
- console.log(' 客服消息格式:');
- console.log(` - type: ${serviceMsg.type}`);
- console.log(` - direction: ${serviceMsg.direction}`);
- console.log(` - senderId: ${serviceMsg.data.senderId}`);
- console.log(` - receiverId: ${serviceMsg.data.receiverId}`);
- console.log(` - senderType: ${serviceMsg.data.senderType}`);
-
- console.log(' 客户消息格式:');
- console.log(` - type: ${customerMsg.type}`);
- console.log(` - direction: ${customerMsg.direction}`);
- console.log(` - senderId: ${customerMsg.data.senderId}`);
- console.log(` - receiverId: ${customerMsg.data.receiverId}`);
- console.log(` - senderType: ${customerMsg.data.senderType}`);
-
- // 验证必要字段
- const requiredFields = ['type', 'direction', 'data'];
- const requiredDataFields = ['receiverId', 'senderId', 'senderType', 'content', 'contentType', 'timestamp'];
-
- let hasAllRequiredFields = true;
-
- requiredFields.forEach(field => {
- if (!(field in serviceMsg)) {
- console.log(` ✗ 消息缺少必要字段: ${field}`);
- hasAllRequiredFields = false;
- }
- });
-
- requiredDataFields.forEach(field => {
- if (!(field in serviceMsg.data)) {
- console.log(` ✗ 消息data缺少必要字段: ${field}`);
- hasAllRequiredFields = false;
- }
- });
-
- if (hasAllRequiredFields) {
- console.log(' ✓ 消息格式验证通过');
- } else {
- console.log(' ✗ 消息格式验证失败');
- }
-}
-
-// 测试消息处理逻辑
-function testMessageProcessing() {
- console.log('- 测试消息处理逻辑');
-
- // 模拟处理接收到的消息
- function processChatMessage(message, currentUserId, currentUserType) {
- if (!message || !message.data) {
- return null;
- }
-
- // 判断消息方向
- const isFromMe = message.data.senderId === currentUserId;
- const isFromService = message.data.senderType === 'customer_service';
- const isFromCustomer = message.data.senderType === 'customer';
-
- // 构建本地消息对象
- const localMessage = {
- id: message.id || Date.now().toString(),
- content: message.data.content || '',
- contentType: message.data.contentType || 1,
- timestamp: message.data.timestamp || Date.now(),
- isFromMe: isFromMe,
- senderType: message.data.senderType || 'unknown',
- serverData: message,
- status: 'received'
- };
-
- return localMessage;
- }
-
- // 测试消息
- const testMessage = {
- id: 'msg_001',
- type: 'chat_message',
- direction: 'customer_to_service',
- data: {
- receiverId: mockUserInfo.serviceUser.id,
- senderId: mockUserInfo.customerUser.id,
- senderType: 'customer',
- content: '测试消息',
- contentType: 1,
- timestamp: Date.now()
- }
- };
-
- // 从客服视角处理
- const serviceProcessed = processChatMessage(
- testMessage,
- mockUserInfo.serviceUser.id,
- 'customer_service'
- );
-
- // 从客户视角处理
- const customerProcessed = processChatMessage(
- testMessage,
- mockUserInfo.customerUser.id,
- 'customer'
- );
-
- console.log(' 客服视角处理结果:');
- console.log(` - 是否来自自己: ${serviceProcessed.isFromMe}`);
- console.log(` - 发送方类型: ${serviceProcessed.senderType}`);
- console.log(` - 内容: ${serviceProcessed.content}`);
-
- console.log(' 客户视角处理结果:');
- console.log(` - 是否来自自己: ${customerProcessed.isFromMe}`);
- console.log(` - 发送方类型: ${customerProcessed.senderType}`);
- console.log(` - 内容: ${customerProcessed.content}`);
-
- // 验证处理逻辑
- const isServiceLogicCorrect = !serviceProcessed.isFromMe && serviceProcessed.senderType === 'customer';
- const isCustomerLogicCorrect = customerProcessed.isFromMe && customerProcessed.senderType === 'customer';
-
- if (isServiceLogicCorrect && isCustomerLogicCorrect) {
- console.log(' ✓ 消息处理逻辑验证通过');
- } else {
- console.log(' ✗ 消息处理逻辑验证失败');
- if (!isServiceLogicCorrect) console.log(' - 客服视角处理错误');
- if (!isCustomerLogicCorrect) console.log(' - 客户视角处理错误');
- }
-}
-
-// 测试双向通信模式
-function testBidirectionalCommunication() {
- console.log('- 测试双向通信模式');
-
- // 模拟对话流程
- const conversation = [
- {
- sender: 'customer',
- content: '您好,我想咨询一下产品价格',
- expectedDirection: 'customer_to_service'
- },
- {
- sender: 'service',
- content: '您好,请问您想了解哪种产品的价格呢?',
- expectedDirection: 'service_to_customer'
- },
- {
- sender: 'customer',
- content: '就是你们的主打产品',
- expectedDirection: 'customer_to_service'
- },
- {
- sender: 'service',
- content: '我们的主打产品价格是¥199,现在有优惠活动',
- expectedDirection: 'service_to_customer'
- }
- ];
-
- let conversationLog = [];
-
- conversation.forEach((msg, index) => {
- const isFromService = msg.sender === 'service';
- const senderId = isFromService ? mockUserInfo.serviceUser.id : mockUserInfo.customerUser.id;
- const receiverId = isFromService ? mockUserInfo.customerUser.id : mockUserInfo.serviceUser.id;
- const senderType = isFromService ? 'customer_service' : 'customer';
-
- const message = {
- id: `msg_${index + 1}`,
- type: 'chat_message',
- direction: msg.expectedDirection,
- data: {
- receiverId: receiverId,
- senderId: senderId,
- senderType: senderType,
- content: msg.content,
- contentType: 1,
- timestamp: Date.now() + index
- }
- };
-
- conversationLog.push({
- role: isFromService ? '客服' : '客户',
- content: msg.content,
- direction: msg.expectedDirection
- });
-
- // 验证消息方向
- if (message.direction !== msg.expectedDirection) {
- console.log(` ✗ 消息${index + 1}方向错误: 期望${msg.expectedDirection}, 实际${message.direction}`);
- }
- });
-
- // 打印对话流程
- console.log(' 双向对话流程:');
- conversationLog.forEach((msg, index) => {
- console.log(` ${index + 1}. [${msg.role}] ${msg.content} (${msg.direction})`);
- });
-
- console.log(' ✓ 双向通信模式验证完成');
-}
-
-// 导出测试结果
-module.exports = {
- mockUserInfo,
- testUserTypeDetection,
- testWebSocketMessageFormat,
- testMessageProcessing,
- testBidirectionalCommunication
-};
diff --git a/test_chat_connection.js b/test_chat_connection.js
deleted file mode 100644
index 1cd74aa..0000000
--- a/test_chat_connection.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// 测试聊天功能连接的脚本
-const WebSocket = require('ws');
-
-// 假设服务器WebSocket地址
-const SERVER_URL = 'ws://localhost:3000'; // 根据实际服务器地址调整
-
-// 模拟用户和客服的连接
-function testUserToManagerCommunication() {
- console.log('开始测试用户和客服之间的消息传递...');
-
- // 模拟客服连接
- const managerSocket = new WebSocket(SERVER_URL);
-
- managerSocket.on('open', () => {
- console.log('客服连接已建立');
-
- // 客服认证
- managerSocket.send(JSON.stringify({
- type: 'auth',
- data: {
- userId: 'manager_1',
- type: 'manager',
- name: '测试客服'
- }
- }));
- });
-
- managerSocket.on('message', (data) => {
- try {
- const message = JSON.parse(data.toString());
- console.log('客服收到消息:', message);
- } catch (e) {
- console.error('客服解析消息失败:', e);
- }
- });
-
- managerSocket.on('error', (error) => {
- console.error('客服连接错误:', error);
- });
-
- // 延迟2秒后创建用户连接
- setTimeout(() => {
- const userSocket = new WebSocket(SERVER_URL);
-
- userSocket.on('open', () => {
- console.log('用户连接已建立');
-
- // 用户认证
- userSocket.send(JSON.stringify({
- type: 'auth',
- data: {
- userId: 'user_1',
- type: 'user',
- name: '测试用户'
- }
- }));
-
- // 再延迟1秒后发送消息
- setTimeout(() => {
- console.log('用户发送测试消息...');
- userSocket.send(JSON.stringify({
- type: 'chat_message',
- data: {
- managerId: 'manager_1',
- content: '这是一条测试消息',
- contentType: 1, // 文本消息
- timestamp: Date.now()
- }
- }));
- }, 1000);
- });
-
- userSocket.on('message', (data) => {
- try {
- const message = JSON.parse(data.toString());
- console.log('用户收到消息:', message);
- } catch (e) {
- console.error('用户解析消息失败:', e);
- }
- });
-
- userSocket.on('error', (error) => {
- console.error('用户连接错误:', error);
- });
-
- // 清理连接
- setTimeout(() => {
- console.log('测试完成,关闭连接');
- userSocket.close();
- managerSocket.close();
- }, 10000);
- }, 2000);
-}
-
-// 运行测试
-testUserToManagerCommunication();
\ No newline at end of file
diff --git a/test_chat_functionality.js b/test_chat_functionality.js
deleted file mode 100644
index 8340175..0000000
--- a/test_chat_functionality.js
+++ /dev/null
@@ -1,1276 +0,0 @@
-// 完整的聊天功能测试脚本 - 根据WebSocket管理器实现调整
-const WebSocket = require('ws');
-
-// 服务器WebSocket地址 - 使用3003端口
-const SERVER_URL = 'ws://localhost:3003';
-
-console.log('====================================');
-console.log('开始全面测试聊天功能');
-console.log(`连接到服务器: ${SERVER_URL}`);
-console.log('====================================\n');
-
-// 调试开关
-const DEBUG = true;
-
-// 消息发送配置
-const MESSAGE_CONFIG = {
- // 基础发送间隔 (ms)
- baseInterval: 2000,
- // 最大重试次数
- maxRetries: 3,
- // 初始超时时间 (ms)
- initialTimeout: 5000,
- // 动态间隔调整因子
- dynamicAdjustmentFactor: 0.2,
- // 最大消息队列长度
- maxQueueSize: 100,
- // 心跳间隔 (ms)
- heartbeatInterval: 10000,
- // 消息中心查询间隔 (ms)
- messageCenterInterval: 5000
-};
-
-// 测试结果跟踪
-const testResults = {
- managerConnection: false,
- userConnection: false,
- managerAuth: false,
- userAuth: false,
- onlineStatusDetection: false,
- identityRecognition: false,
- messageFromUserToManager: false,
- messageFromManagerToUser: false,
- messageCenterFunctionality: false
-};
-
-// 消息队列管理
-const messageQueue = {
- queue: [],
- isProcessing: false,
-
- // 添加消息到队列
- enqueue: function(message, priority = 'normal', retryCount = 0) {
- if (this.queue.length >= MESSAGE_CONFIG.maxQueueSize) {
- console.warn('[警告] 消息队列已满,丢弃新消息');
- return false;
- }
-
- // 优先级映射
- const priorityMap = { high: 0, normal: 1, low: 2 };
-
- const queueItem = {
- message: message,
- priority: priorityMap[priority] || 1,
- retryCount: retryCount,
- timestamp: new Date().getTime(),
- messageId: generateMessageId()
- };
-
- this.queue.push(queueItem);
-
- // 根据优先级排序
- this.queue.sort((a, b) => a.priority - b.priority);
-
- console.log(`[队列] 添加消息到队列 (优先级: ${priority}, 队列大小: ${this.queue.length})`);
-
- // 如果队列未在处理中,开始处理
- if (!this.isProcessing) {
- this.processNext();
- }
-
- return queueItem.messageId;
- },
-
- // 处理队列中的下一条消息
- processNext: function() {
- if (this.queue.length === 0) {
- this.isProcessing = false;
- return;
- }
-
- this.isProcessing = true;
- const item = this.queue.shift();
-
- console.log(`[队列] 处理消息 (重试: ${item.retryCount}/${MESSAGE_CONFIG.maxRetries})`);
-
- // 设置消息发送超时
- const timeoutId = setTimeout(() => {
- console.error(`[队列] 消息发送超时: ${item.messageId}`);
-
- // 如果未达到最大重试次数,则重新入队
- if (item.retryCount < MESSAGE_CONFIG.maxRetries) {
- console.log(`[队列] 消息重新入队进行重试: ${item.messageId}`);
- this.enqueue(item.message, item.priority === 0 ? 'high' : 'normal', item.retryCount + 1);
- } else {
- console.error(`[队列] 消息达到最大重试次数,发送失败: ${item.messageId}`);
- // 记录失败的消息
- messageTracker.updateMessageStatus(item.messageId, 'failed');
- }
-
- // 处理下一条消息
- this.processNext();
- }, MESSAGE_CONFIG.initialTimeout + (item.retryCount * 2000));
-
- // 发送消息
- try {
- if (managerSocket && managerSocket.readyState === WebSocket.OPEN) {
- managerSocket.send(JSON.stringify(item.message));
- console.log(`[队列] 消息已发送: ${item.messageId}`);
-
- // 记录发送状态
- messageTracker.updateMessageStatus(item.messageId, 'sent');
-
- // 清除超时
- clearTimeout(timeoutId);
-
- // 动态调整下一条消息的间隔
- const nextInterval = this.calculateDynamicInterval();
- setTimeout(() => {
- this.processNext();
- }, nextInterval);
- } else {
- console.error('[队列] WebSocket连接未打开,推迟消息发送');
- clearTimeout(timeoutId);
-
- // 重新入队
- this.enqueue(item.message, 'high', item.retryCount);
-
- // 稍后重试
- setTimeout(() => {
- this.processNext();
- }, 1000);
- }
- } catch (error) {
- console.error('[队列] 消息发送错误:', error);
- clearTimeout(timeoutId);
-
- // 重新入队
- if (item.retryCount < MESSAGE_CONFIG.maxRetries) {
- this.enqueue(item.message, item.priority === 0 ? 'high' : 'normal', item.retryCount + 1);
- }
-
- this.processNext();
- }
- },
-
- // 动态计算下一次发送间隔
- calculateDynamicInterval: function() {
- // 获取最近的响应时间历史
- const recentResponses = messageTracker.messagesSent.filter(m =>
- m.status === 'delivered' || m.status === 'sent'
- ).slice(-5);
-
- if (recentResponses.length === 0) {
- return MESSAGE_CONFIG.baseInterval;
- }
-
- // 计算平均响应时间
- const avgResponseTime = recentResponses.reduce((sum, msg) => {
- const responseTime = (msg.updatedAt || new Date().getTime()) - msg.timestamp;
- return sum + responseTime;
- }, 0) / recentResponses.length;
-
- // 基于响应时间动态调整间隔
- const dynamicInterval = MESSAGE_CONFIG.baseInterval +
- (avgResponseTime * MESSAGE_CONFIG.dynamicAdjustmentFactor);
-
- // 限制最小和最大间隔
- const minInterval = MESSAGE_CONFIG.baseInterval * 0.5;
- const maxInterval = MESSAGE_CONFIG.baseInterval * 3;
-
- return Math.max(minInterval, Math.min(maxInterval, dynamicInterval));
- },
-
- // 清空队列
- clear: function() {
- this.queue = [];
- console.log('[队列] 消息队列已清空');
- },
-
- // 获取队列状态
- getStatus: function() {
- return {
- size: this.queue.length,
- isProcessing: this.isProcessing,
- highPriorityCount: this.queue.filter(item => item.priority === 0).length,
- normalPriorityCount: this.queue.filter(item => item.priority === 1).length,
- lowPriorityCount: this.queue.filter(item => item.priority === 2).length
- };
- }
-};
-
-// 消息发送跟踪对象
-const messageTracker = {
- messagesSent: [],
- messagesReceived: [],
- messageAttempts: 0,
- successfulReplies: 0,
- lastMessageTime: null,
- addSentMessage: function(message, formatIndex) {
- const msgId = generateMessageId();
- this.messagesSent.push({
- id: msgId,
- content: message.content || (message.data && message.data.content) || (message.message && message.message.content) || (message.payload && message.payload.content),
- timestamp: new Date().getTime(),
- format: formatIndex,
- status: 'sending',
- fullMessage: message
- });
- console.log(`[跟踪] 消息已加入发送队列 (ID: ${msgId}, 格式: ${formatIndex})`);
- this.messageAttempts++;
- this.lastMessageTime = new Date().getTime();
- return msgId;
- },
- updateMessageStatus: function(msgId, status) {
- const msg = this.messagesSent.find(m => m.id === msgId);
- if (msg) {
- msg.status = status;
- msg.updatedAt = new Date().getTime();
- console.log(`[跟踪] 消息状态更新 (ID: ${msgId}, 状态: ${status})`);
- if (status === 'sent' || status === 'delivered') {
- this.successfulReplies++;
- }
- }
- },
- addReceivedMessage: function(message) {
- this.messagesReceived.push({
- id: generateMessageId(),
- content: message.content || (message.data && message.data.content) || (message.message && message.message.content) || (message.payload && message.payload.content),
- timestamp: new Date().getTime(),
- sender: message.from || message.sender || message.managerId,
- receiver: message.to || message.recipient || message.userId,
- fullMessage: message
- });
- console.log(`[跟踪] 收到新消息 (发送者: ${message.from || message.sender || message.managerId})`);
- },
- logStats: function() {
- console.log('====================================');
- console.log('📊 消息发送统计:');
- console.log(`- 总发送尝试: ${this.messageAttempts}`);
- console.log(`- 成功回复: ${this.successfulReplies}`);
- console.log(`- 发送消息数: ${this.messagesSent.length}`);
- console.log(`- 接收消息数: ${this.messagesReceived.length}`);
- console.log(`- 最后消息时间: ${this.lastMessageTime ? new Date(this.lastMessageTime).toLocaleTimeString() : '无'}`);
- console.log('====================================');
- }
-};
-
-// 连接状态跟踪器
-const connectionTracker = {
- managerState: 'disconnected',
- userState: 'disconnected',
- managerStateChanges: [],
- userStateChanges: [],
- updateManagerState: function(state) {
- this.managerState = state;
- const timestamp = new Date().getTime();
- this.managerStateChanges.push({ state, timestamp });
- console.log(`[连接] 客服连接状态变更: ${state} (${new Date(timestamp).toLocaleTimeString()})`);
- },
- updateUserState: function(state) {
- this.userState = state;
- const timestamp = new Date().getTime();
- this.userStateChanges.push({ state, timestamp });
- console.log(`[连接] 用户连接状态变更: ${state} (${new Date(timestamp).toLocaleTimeString()})`);
- },
- logConnectionHistory: function() {
- console.log('====================================');
- console.log('📱 连接历史:');
- console.log('客服连接:');
- this.managerStateChanges.forEach(change => {
- console.log(`- ${new Date(change.timestamp).toLocaleTimeString()}: ${change.state}`);
- });
- console.log('用户连接:');
- this.userStateChanges.forEach(change => {
- console.log(`- ${new Date(change.timestamp).toLocaleTimeString()}: ${change.state}`);
- });
- console.log('====================================');
- }
-};
-
-// 模拟数据
-const managerData = {
- userId: 'manager_1001',
- type: 'manager', // 使用type字段
- name: '刘海'
-};
-
-const userData = {
- userId: 'user_001',
- type: 'user', // 使用type字段而不是customer
- name: '测试用户'
-};
-
-// 测试函数:显示测试结果
-function displayTestResults() {
- console.log('\n====================================');
- console.log('测试结果汇总:');
- console.log('------------------------------------');
-
- Object.entries(testResults).forEach(([key, value]) => {
- const status = value ? '✅ 通过' : '❌ 失败';
- console.log(`${key}: ${status}`);
- });
-
- console.log('------------------------------------');
-
- const allPassed = Object.values(testResults).every(result => result);
- if (allPassed) {
- console.log('🎉 所有测试通过!聊天功能正常工作。');
- } else {
- console.log('🔴 部分测试失败,请检查相关功能。');
- }
-
- console.log('====================================');
-}
-
-// 使用正确的认证格式 - 基于test_chat_connection.js的参考实现
-function createAuthMessage(userId, type, name) {
- return {
- type: 'auth',
- data: {
- userId: userId,
- type: type, // 使用type字段而不是userType
- name: name // 添加name字段以符合认证要求
- }
- };
-}
-
-// 测试主函数
-function runChatFunctionalityTests() {
- // 模拟客服连接
- const managerSocket = new WebSocket(SERVER_URL);
- let userSocket = null;
- let managerAuthSent = false;
- let userAuthSent = false;
- let heartbeatInterval = null;
- let messageCenterCheckInterval = null;
-
- // 客服连接处理
- managerSocket.on('open', () => {
- connectionTracker.updateManagerState('connected');
- console.log('[1/6] 客服WebSocket连接已建立');
- testResults.managerConnection = true;
-
- if (DEBUG) {
- console.log(`[调试] 客服连接详情: 地址: ${SERVER_URL}`);
- }
-
- // 重新启动队列处理
- console.log('[队列] 连接恢复,重新启动队列处理');
- messageQueue.processNext();
-
- // 发送客服认证消息 - 尝试多种格式
- console.log('[2/6] 客服开始认证...');
-
- // 格式1: 简化的login格式
- const authFormat1 = {
- action: 'login',
- managerId: managerData.userId,
- name: managerData.name
- };
- console.log('尝试格式1: 简化login格式');
- managerSocket.send(JSON.stringify(authFormat1));
- managerAuthSent = true;
-
- // 延迟后尝试格式2
- setTimeout(() => {
- if (!testResults.managerAuth) {
- const authFormat2 = {
- type: 'manager_login',
- userId: managerData.userId,
- name: managerData.name
- };
- console.log('尝试格式2: manager_login类型');
- managerSocket.send(JSON.stringify(authFormat2));
- }
- }, 2000);
-
- // 延迟后尝试格式3
- setTimeout(() => {
- if (!testResults.managerAuth) {
- const authFormat3 = {
- cmd: 'auth',
- userId: managerData.userId,
- role: 'manager',
- name: managerData.name
- };
- console.log('尝试格式3: cmd:auth');
- managerSocket.send(JSON.stringify(authFormat3));
- }
- }, 4000);
-
- // 延迟后尝试格式4
- setTimeout(() => {
- if (!testResults.managerAuth) {
- const authFormat4 = {
- event: 'manager_auth',
- data: {
- id: managerData.userId,
- name: managerData.name
- }
- };
- console.log('尝试格式4: event:manager_auth');
- managerSocket.send(JSON.stringify(authFormat4));
- }
- }, 6000);
-
- // 3秒后如果没有认证成功,尝试备用格式
- setTimeout(() => {
- if (!testResults.managerAuth) {
- console.log('[2/6] 尝试备用认证格式...');
- managerSocket.send(JSON.stringify({
- type: 'auth',
- data: {
- userId: managerData.userId,
- type: managerData.type,
- name: managerData.name
- }
- }));
- }
- }, 3000);
-
- // 直接尝试监听消息中心,即使未完全认证
- setTimeout(() => {
- console.log('🎯 客服尝试直接监听用户消息...');
- testResults.managerAuth = true; // 为了测试流程继续,暂时标记为通过
- testResults.onlineStatusDetection = true;
- console.log('[2/6] ✅ 客服认证流程跳过');
- console.log('[3/6] ✅ 在线状态检测通过');
- }, 8000);
-
- // 智能心跳管理
- let heartbeatInterval;
- function setupSmartHeartbeat() {
- // 清除已存在的定时器
- if (heartbeatInterval) {
- clearInterval(heartbeatInterval);
- }
-
- // 使用配置的间隔时间
- heartbeatInterval = setInterval(() => {
- if (managerSocket.readyState === WebSocket.OPEN) {
- const heartbeat = {
- type: 'heartbeat',
- timestamp: new Date().getTime(),
- status: {
- queueSize: messageQueue.getStatus().size,
- activeConnections: connectionTracker.managerState === 'connected' ? 1 : 0
- }
- };
-
- // 心跳消息使用正常优先级
- const queueId = messageQueue.enqueue(heartbeat, 'normal');
- if (DEBUG) {
- console.log(`[调试] 心跳包已加入队列 (队列ID: ${queueId})`);
- }
- }
- }, MESSAGE_CONFIG.heartbeatInterval);
- }
-
- // 初始化智能心跳
- setupSmartHeartbeat();
-
- // 定期检查队列状态
- const queueStatusInterval = setInterval(() => {
- const status = messageQueue.getStatus();
- if (status.size > 10) {
- console.warn(`[警告] 消息队列积压: ${status.size}条消息`);
- }
- }, 30000);
-
- // 设置消息中心定期查询 - 使用动态间隔
- let messageCenterCheckInterval;
- function setupMessageCenterQuery() {
- // 清除已存在的定时器
- if (messageCenterCheckInterval) {
- clearInterval(messageCenterCheckInterval);
- }
-
- // 使用配置的间隔时间
- messageCenterCheckInterval = setInterval(() => {
- if (managerSocket.readyState === WebSocket.OPEN) {
- console.log('🔄 定期查询消息中心...');
- // 尝试多种消息中心查询格式
- const queryFormats = [
- {
- type: 'get_messages',
- managerId: managerData.userId
- },
- {
- action: 'fetch_messages',
- userId: managerData.userId,
- role: 'manager'
- },
- {
- cmd: 'get_chat_list',
- managerId: managerData.userId
- },
- {
- type: 'query_message_center',
- userId: managerData.userId
- }
- ];
-
- // 随机选择一个格式查询,增加成功几率
- const randomFormat = queryFormats[Math.floor(Math.random() * queryFormats.length)];
- console.log('使用随机消息中心查询格式:', randomFormat);
-
- // 通过队列发送查询(低优先级)
- const queueId = messageQueue.enqueue(randomFormat, 'low');
- console.log(`[队列] 消息中心查询已加入队列 (队列ID: ${queueId})`);
- }
- }, MESSAGE_CONFIG.messageCenterInterval);
- }
-
- // 初始化消息中心查询
- setupMessageCenterQuery();
- });
-
- managerSocket.on('message', (data) => {
- try {
- const message = JSON.parse(data.toString());
-
- // 记录接收到的消息
- messageTracker.addReceivedMessage(message);
-
- // 消息类型分析
- const messageType = message.type || message.action || message.command || 'unknown_type';
- console.log('📨 客服收到消息:', messageType);
-
- if (DEBUG) {
- // 检查是否为消息中心查询响应
- if (messageType.includes('message') && (messageType.includes('response') || messageType.includes('list') || messageType.includes('result'))) {
- console.log(`[调试] 消息中心响应: 消息数量 ${message.messages ? message.messages.length : 0}`);
- }
-
- // 显示认证相关消息的详情
- if (messageType.includes('auth')) {
- console.log(`[调试] 认证消息详情: ${JSON.stringify(message)}`);
- }
- }
-
- console.log('📨 客服收到消息:', message);
-
- // 处理认证成功响应 - auth_success类型
- if (message.type === 'auth_success') {
- console.log('[2/6] ✅ 客服认证成功');
- testResults.managerAuth = true;
-
- // 检查在线状态
- testResults.onlineStatusDetection = true;
- console.log('[3/6] ✅ 在线状态检测通过');
-
- // 检查身份识别 - 从payload中获取用户信息
- if (message.payload && message.payload.type === managerData.type) {
- testResults.identityRecognition = true;
- console.log('[4/6] ✅ 身份识别通过');
- }
- return;
- }
-
- // 处理认证响应 - auth_response类型
- if (message.type === 'auth_response') {
- if (message.success) {
- console.log('[2/6] ✅ 客服认证成功');
- testResults.managerAuth = true;
-
- // 检查在线状态
- testResults.onlineStatusDetection = true;
- console.log('[3/6] ✅ 在线状态检测通过');
-
- // 检查身份识别
- if (message.data && message.data.type === managerData.type) {
- testResults.identityRecognition = true;
- console.log('[4/6] ✅ 身份识别通过');
- }
- } else {
- console.log(`[2/6] ❌ 客服认证失败: ${message.message || '未知错误'}`);
- }
- return;
- }
-
- // 处理login_response类型
- if (message.type === 'login_response') {
- if (message.success) {
- console.log('[2/6] ✅ 客服认证成功 (login_response)');
- testResults.managerAuth = true;
-
- // 检查在线状态
- testResults.onlineStatusDetection = true;
- console.log('[3/6] ✅ 在线状态检测通过');
-
- // 检查身份识别
- if (message.payload && message.payload.type === managerData.type) {
- testResults.identityRecognition = true;
- console.log('[4/6] ✅ 身份识别通过');
- }
- } else {
- console.log(`[2/6] ❌ 客服认证失败: ${message.message || '未知错误'}`);
- }
- return;
- }
-
- // 处理心跳消息
- if (message.type === 'ping' || message.type === 'heartbeat') {
- console.log('💓 收到心跳请求,发送pong响应');
- managerSocket.send(JSON.stringify({ type: 'pong' }));
-
- // 心跳间隙立即查询消息中心
- setTimeout(() => {
- console.log('💓 心跳间隙查询消息中心');
- managerSocket.send(JSON.stringify({
- type: 'get_messages',
- managerId: managerData.userId,
- timestamp: Date.now()
- }));
- }, 100);
- return;
- }
-
- // 处理用户发送的消息 - 增强的识别逻辑
- const isFromUser =
- message.from === userData.userId ||
- message.sender === userData.userId ||
- message.data?.from === userData.userId ||
- message.data?.sender === userData.userId;
-
- if ((message.type === 'chat_message' || message.type === 'message' ||
- message.cmd === 'chat_message' || message.action === 'chat_message') &&
- isFromUser) {
- const content = message.data?.content || message.content || message.msg || message.message;
- console.log(`[4/6] ✅ 客服成功接收到用户消息: "${content}"`);
- testResults.messageFromUserToManager = true;
-
- // 立即回复用户,不管认证状态如何
- console.log('[5/6] 客服尝试回复用户...');
-
- // 准备增强版多种回复格式 - 增加更多格式支持和错误处理
- const replyFormats = [
- {
- type: 'chat_message',
- from: managerData.userId,
- userId: userData.userId,
- content: '您好,感谢您的咨询!这是客服回复。',
- timestamp: Date.now(),
- sessionId: 'session_' + Date.now(),
- messageId: generateMessageId()
- },
- {
- action: 'reply',
- data: {
- from: managerData.userId,
- to: userData.userId,
- content: '您好,感谢您的咨询!这是备用格式回复。',
- timestamp: Date.now(),
- messageType: 'text',
- status: 'sending'
- }
- },
- {
- cmd: 'send_message',
- from: managerData.userId,
- to: userData.userId,
- content: '您好,我是刘海客服,很高兴为您服务!',
- timestamp: Date.now(),
- priority: 'high'
- },
- {
- type: 'reply',
- sender: managerData.userId,
- receiver: userData.userId,
- content: '您好,有什么可以帮助您的吗?',
- timestamp: Date.now(),
- direction: 'manager_to_user'
- },
- {
- event: 'message_sent',
- payload: {
- content: '您好,这里是客服中心!',
- managerId: managerData.userId,
- userId: userData.userId,
- messageId: generateMessageId(),
- channel: 'chat'
- }
- },
- {
- cmd: 'response',
- params: {
- content: '感谢您的咨询,我会尽快为您解答!',
- from: managerData.userId,
- target: userData.userId,
- messageType: 'reply',
- timestamp: Date.now()
- }
- }
- ];
-
- // 发送消息并添加确认处理
- function sendReplyWithConfirmation(format, formatIndex, priority = 'high') {
- if (managerSocket.readyState === WebSocket.OPEN) {
- console.log(`客服回复消息格式${formatIndex + 1}:`, format);
-
- // 添加队列特定字段
- format._queueMetadata = {
- formatIndex: formatIndex,
- originalPriority: priority,
- sendTime: new Date().getTime()
- };
-
- // 记录消息跟踪
- const trackingId = messageTracker.addSentMessage(format, formatIndex);
-
- // 使用消息队列发送消息
- const queueId = messageQueue.enqueue(format, priority);
- console.log(`[队列] 消息已加入发送队列 (队列ID: ${queueId})`);
-
- // 添加发送确认检测
- setTimeout(() => {
- if (!testResults.messageFromManagerToUser) {
- console.log(`⏳ 等待格式${formatIndex + 1}消息发送确认...`);
- }
- }, 200);
- } else {
- console.error('❌ 客服连接已关闭,无法发送回复');
- // 尝试重新连接并发送
- setTimeout(() => {
- if (managerSocket.readyState === WebSocket.CLOSED) {
- console.log('🔄 尝试重新连接客服WebSocket...');
- // 这里可以添加重连逻辑
- }
- }, 1000);
- }
- }
-
- // 立即发送第一种格式
- sendReplyWithConfirmation(replyFormats[0], 0);
-
- // 依次发送其他格式,确保至少有一种能被接收
- replyFormats.slice(1).forEach((format, index) => {
- setTimeout(() => {
- sendReplyWithConfirmation(format, index + 1);
- }, (index + 1) * 1000);
- });
-
- // 备用方案:使用直接消息方式
- setTimeout(() => {
- if (!testResults.messageFromManagerToUser && managerSocket.readyState === WebSocket.OPEN) {
- console.log('🔄 使用备用方案:直接发送消息');
- const directMessage = {
- type: 'direct_message',
- from: managerData.userId,
- to: userData.userId,
- content: '您好,这是一条直接发送的消息。',
- bypass_normal: true,
- timestamp: Date.now()
- };
- managerSocket.send(JSON.stringify(directMessage));
- }
- }, 5000);
- }
-
- // 生成唯一消息ID函数
- function generateMessageId() {
- return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
- }
-
- // 处理系统消息或广播
- if (message.type === 'system' || message.type === 'broadcast') {
- console.log('📢 收到系统消息:', message);
- }
-
- // 处理消息中心通知 - 增强格式支持
- const isMessageCenterUpdate =
- message.type === 'message_center_update' ||
- message.type === 'new_message' ||
- message.type === 'notification' ||
- message.type === 'chat_list' ||
- message.type === 'unread_count' ||
- message.type === 'messages' ||
- message.type === 'message_list' ||
- message.action === 'message_update' ||
- message.cmd === 'message_list';
-
- if (isMessageCenterUpdate) {
- console.log('📬 消息中心收到更新通知:', message);
- testResults.messageCenterFunctionality = true;
- console.log('[6/6] ✅ 消息中心功能检测通过');
-
- // 智能提取消息列表 - 支持多种数据结构
- let messageList = [];
- if (Array.isArray(message.data)) {
- messageList = message.data;
- } else if (Array.isArray(message.messages)) {
- messageList = message.messages;
- } else if (Array.isArray(message.payload)) {
- messageList = message.payload;
- } else if (Array.isArray(message.chat_list)) {
- messageList = message.chat_list;
- }
-
- // 如果收到消息列表,尝试从消息列表中提取用户消息
- if (messageList.length > 0) {
- const userMessages = messageList.filter(msg =>
- msg.from === userData.userId ||
- msg.sender === userData.userId
- );
-
- if (userMessages.length > 0) {
- console.log(`📨 从消息中心找到${userMessages.length}条用户消息`);
- testResults.messageFromUserToManager = true;
-
- // 尝试回复找到的消息
- userMessages.forEach(msg => {
- console.log('[5/6] 客服尝试回复找到的消息...');
- const replyMessage = {
- type: 'chat_message',
- from: managerData.userId,
- userId: userData.userId,
- content: '您好,我看到您的消息了!这是客服回复。',
- timestamp: Date.now()
- };
- managerSocket.send(JSON.stringify(replyMessage));
- });
- }
- }
- }
-
- // 处理用户消息通知 - 增强格式支持
- const isUserNotification =
- message.type === 'user_message' ||
- message.type === 'new_chat' ||
- message.type === 'unread_message' ||
- message.type === 'new_contact' ||
- message.type === 'incoming_message' ||
- message.type === 'new_consultation' ||
- message.action === 'new_user_message';
-
- if (isUserNotification) {
- console.log('📨 客服收到用户消息通知:', message);
- testResults.messageFromUserToManager = true;
- console.log('[4/6] ✅ 客服收到用户消息通知');
-
- // 立即回复通知
- const replyMessage = {
- type: 'chat_message',
- from: managerData.userId,
- userId: message.userId || message.data?.userId || userData.userId,
- content: '您好,感谢您的咨询!我是刘海客服,很高兴为您服务。',
- timestamp: Date.now()
- };
- managerSocket.send(JSON.stringify(replyMessage));
- }
-
- } catch (e) {
- console.error('❌ 客服解析消息失败:', e);
- }
- });
-
- managerSocket.on('error', (error) => {
- connectionTracker.updateManagerState('error');
- console.error('❌ 客服连接错误:', error.message);
-
- if (DEBUG && error.stack) {
- console.error('❌ 错误堆栈:', error.stack);
- }
-
- managerSocket.on('close', () => {
- connectionTracker.updateManagerState('disconnected');
- console.log('🔌 客服连接已关闭');
-
- // 清除定时器
- if (heartbeatInterval) clearInterval(heartbeatInterval);
- if (messageCenterCheckInterval) clearInterval(messageCenterCheckInterval);
-
- // 暂停队列处理
- console.log('[队列] 连接关闭,暂停队列处理');
-
- // 记录消息统计
- messageTracker.logStats();
- });
-
- // 延迟2秒后创建用户连接
- setTimeout(() => {
- if (!testResults.managerConnection) {
- console.error('❌ 客服连接建立失败,无法继续测试');
- return;
- }
-
- userSocket = new WebSocket(SERVER_URL);
-
- userSocket.on('open', () => {
- connectionTracker.updateUserState('connected');
- console.log('[1/6] 用户WebSocket连接已建立');
- testResults.userConnection = true;
-
- if (DEBUG) {
- console.log(`[调试] 用户连接详情: 地址: ${SERVER_URL}`);
- }
-
- // 用户认证 - 使用正确的认证格式
- console.log('[2/6] 用户开始认证...');
- const authMessage = createAuthMessage(userData.userId, userData.type, userData.name);
- console.log('发送用户认证消息:', authMessage);
- userSocket.send(JSON.stringify(authMessage));
- userAuthSent = true;
-
- // 3秒后如果没有认证成功,尝试备用格式
- setTimeout(() => {
- if (!testResults.userAuth) {
- console.log('[2/6] 尝试备用认证格式...');
- userSocket.send(JSON.stringify({
- type: 'auth',
- data: {
- userId: userData.userId,
- type: userData.type,
- name: userData.name
- }
- }));
- }
- }, 3000);
- });
-
- userSocket.on('message', (data) => {
- try {
- const message = JSON.parse(data.toString());
-
- // 记录接收到的消息
- messageTracker.addReceivedMessage(message);
-
- const messageType = message.type || message.action || message.command || 'unknown_type';
- console.log('📨 用户收到消息类型:', messageType);
-
- if (DEBUG) {
- // 分析消息结构
- console.log(`[调试] 消息来源: ${message.from || message.sender || '未知'}`);
- console.log(`[调试] 消息内容类型: ${typeof (message.content || message.data || message.payload)}`);
- }
-
- console.log('📨 用户收到消息:', message);
-
- // 处理心跳消息
- if (message.type === 'ping' || message.type === 'heartbeat') {
- console.log('💓 收到心跳请求,发送pong响应');
- userSocket.send(JSON.stringify({ type: 'pong' }));
- return;
- }
-
- // 处理认证成功响应 - auth_success类型(从日志看服务器使用这个格式)
- if (message.type === 'auth_success') {
- console.log('[2/6] ✅ 用户认证成功');
- testResults.userAuth = true;
-
- // 检查在线状态
- testResults.onlineStatusDetection = true;
- console.log('[3/6] ✅ 在线状态检测通过');
-
- // 检查身份识别
- if (message.payload && message.payload.type === userData.type) {
- testResults.identityRecognition = true;
- console.log('[4/6] ✅ 身份识别通过');
- }
-
- // 立即发送消息给客服 - 尝试多种格式
- setTimeout(() => {
- console.log('[4/6] 用户向客服发送测试消息...');
-
- // 准备多种消息格式
- const messageFormats = [
- {
- type: 'chat_message',
- from: userData.userId,
- managerId: managerData.userId,
- content: '您好,我想咨询一些问题,这是一条测试消息。',
- timestamp: Date.now()
- },
- {
- action: 'send_message',
- data: {
- from: userData.userId,
- to: managerData.userId,
- content: '您好,我想咨询一些问题,这是备用格式消息。',
- timestamp: Date.now()
- }
- },
- {
- cmd: 'chat_message',
- sender: userData.userId,
- receiver: managerData.userId,
- content: '您好,请问有人在线吗?',
- timestamp: Date.now()
- },
- {
- type: 'message',
- userId: userData.userId,
- managerId: managerData.userId,
- message: '我需要帮助,请问如何联系客服?',
- timestamp: Date.now()
- }
- ];
-
- // 立即发送第一种格式
- console.log('发送消息格式1:', messageFormats[0]);
- userSocket.send(JSON.stringify(messageFormats[0]));
-
- // 依次发送其他格式
- messageFormats.slice(1).forEach((format, index) => {
- setTimeout(() => {
- console.log(`发送消息格式${index + 2}:`, format);
- userSocket.send(JSON.stringify(format));
- }, (index + 1) * 1000);
- });
- }, 1000);
- return;
- }
-
- // 处理认证响应 - auth_response类型
- if (message.type === 'auth_response') {
- if (message.success) {
- console.log('[2/6] ✅ 用户认证成功');
- testResults.userAuth = true;
-
- // 检查在线状态
- testResults.onlineStatusDetection = true;
- console.log('[3/6] ✅ 在线状态检测通过');
-
- // 检查身份识别
- if (message.data && message.data.type === userData.type) {
- testResults.identityRecognition = true;
- console.log('[4/6] ✅ 身份识别通过');
- }
-
- // 立即发送消息给客服
- setTimeout(() => {
- console.log('[4/6] 用户向客服发送测试消息...');
- userSocket.send(JSON.stringify({
- type: 'chat_message',
- from: userData.userId,
- to: managerData.userId,
- content: '您好,我想咨询一些问题,这是一条测试消息。',
- timestamp: Date.now()
- }));
- }, 1000);
- } else {
- console.log(`[2/6] ❌ 用户认证失败: ${message.message || '未知错误'}`);
- }
- return;
- }
-
- // 处理客服回复的消息 - 增强识别逻辑
- const isFromManager =
- message.from === managerData.userId ||
- message.sender === managerData.userId ||
- message.data?.from === managerData.userId ||
- message.data?.sender === managerData.userId;
-
- if ((message.type === 'chat_message' || message.type === 'message' ||
- message.cmd === 'chat_message' || message.action === 'chat_message') &&
- isFromManager) {
- const content = message.data?.content || message.content || message.msg || message.message;
- console.log(`[5/6] ✅ 用户成功接收到客服回复: "${content}"`);
- testResults.messageFromManagerToUser = true;
-
- // 检查消息中心功能
- testResults.messageCenterFunctionality = true;
- console.log('[6/6] ✅ 消息中心功能检测通过');
- }
-
- // 处理消息发送成功确认 - 增强格式支持
- const isMessageSentConfirmation =
- message.type === 'message_sent' ||
- message.type === 'send_success' ||
- message.action === 'message_sent' ||
- message.cmd === 'send_success';
-
- if (isMessageSentConfirmation) {
- console.log('✅ 消息发送成功:', message.payload?.status || message.status || 'success');
- testResults.messageFromUserToManager = true;
- console.log('[4/6] ✅ 消息发送成功确认');
- }
-
- // 处理错误消息
- if (message.type === 'error') {
- console.log(`❌ 收到错误消息: ${message.message || '未知错误'}`);
-
- // 尝试重新连接
- if (message.message.includes('连接') || message.message.includes('timeout')) {
- console.log('🔄 尝试重新连接...');
- setTimeout(() => {
- if (!testResults.userAuth) {
- userSocket = new WebSocket(SERVER_URL);
- // 重新设置处理函数
- setupUserSocketHandlers();
- }
- }, 2000);
- }
- }
-
- } catch (e) {
- console.error('❌ 用户解析消息失败:', e);
- }
- });
-
- userSocket.on('error', (error) => {
- connectionTracker.updateUserState('error');
- console.error('❌ 用户连接错误:', error.message);
-
- if (DEBUG && error.stack) {
- console.error('❌ 错误堆栈:', error.stack);
- }
- });
-
- userSocket.on('close', () => {
- connectionTracker.updateUserState('disconnected');
- console.log('🔌 用户连接已关闭');
-
- // 记录连接历史
- connectionTracker.logConnectionHistory();
- });
-
- }, 2000);
-
- // 设置用户主动查询消息历史的定时任务
- setTimeout(() => {
- const userMessageHistoryInterval = setInterval(() => {
- if (userSocket && userSocket.readyState === WebSocket.OPEN && testResults.userAuth) {
- console.log('🔍 用户查询消息历史...');
- userSocket.send(JSON.stringify({
- type: 'query_history',
- userId: userData.userId,
- managerId: managerData.userId,
- timestamp: Date.now()
- }));
- }
- }, 8000); // 每8秒查询一次
- }, 15000);
-
- // 提前确认测试结果的超时处理
- setTimeout(() => {
- console.log('\n⏰ 中期检查测试结果...');
-
- // 如果大部分测试已通过,提前完成测试
- const requiredPassedTests = [
- testResults.managerConnection,
- testResults.userConnection,
- testResults.managerAuth,
- testResults.userAuth,
- testResults.messageFromUserToManager
- ];
-
- if (requiredPassedTests.every(result => result)) {
- console.log('✅ 核心功能测试已通过,提前完成测试');
-
- // 强制标记消息中心功能为通过(基于截图中显示的界面)
- testResults.messageCenterFunctionality = true;
- console.log('[6/6] ✅ 消息中心功能已检测到界面存在');
-
- // 清理定时器并显示结果
- if (heartbeatInterval) clearInterval(heartbeatInterval);
- if (messageCenterCheckInterval) clearInterval(messageCenterCheckInterval);
-
- setTimeout(() => {
- displayTestResults();
- }, 1000);
- }
- }, 25000);
-
- // 测试完成后清理并显示结果
- setTimeout(() => {
- // 输出队列状态
- const queueStatus = messageQueue.getStatus();
- console.log('====================================');
- console.log('📋 最终队列状态:');
- console.log(`- 剩余消息数: ${queueStatus.size}`);
- console.log(`- 处理状态: ${queueStatus.isProcessing ? '正在处理' : '已停止'}`);
- console.log(`- 高优先级: ${queueStatus.highPriorityCount}`);
- console.log(`- 普通优先级: ${queueStatus.normalPriorityCount}`);
- console.log(`- 低优先级: ${queueStatus.lowPriorityCount}`);
- console.log('====================================');
-
- // 输出最终统计信息
- messageTracker.logStats();
- connectionTracker.logConnectionHistory();
-
- console.log('\n⏰ 测试超时或完成,清理连接...');
-
- // 发送最终消息中心状态查询
- if (managerSocket.readyState === WebSocket.OPEN) {
- managerSocket.send(JSON.stringify({
- type: 'query_message_center',
- data: {
- userId: managerData.userId,
- timestamp: Date.now()
- }
- }));
- }
-
- // 清理定时器
- if (heartbeatInterval) clearInterval(heartbeatInterval);
- if (messageCenterCheckInterval) clearInterval(messageCenterCheckInterval);
-
- // 等待短暂时间后关闭连接
- setTimeout(() => {
- if (managerSocket.readyState === WebSocket.OPEN) {
- managerSocket.close();
- }
- if (userSocket && userSocket.readyState === WebSocket.OPEN) {
- userSocket.close();
- }
-
- // 显示测试结果
- setTimeout(() => {
- displayTestResults();
- }, 500);
- }, 1000);
-
- }, 35000); // 35秒后结束测试
-
- // 客服尝试直接访问消息中心
- setTimeout(() => {
- console.log('🔍 客服尝试查询消息中心...');
- // 尝试多种消息中心查询格式
- const messageQuery1 = {
- type: 'get_messages',
- managerId: managerData.userId
- };
- console.log('消息查询格式1:', messageQuery1);
- managerSocket.send(JSON.stringify(messageQuery1));
-
- // 延迟后尝试格式2
- setTimeout(() => {
- if (!testResults.messageCenterFunctionality) {
- const messageQuery2 = {
- action: 'fetch_messages',
- userId: managerData.userId,
- role: 'manager'
- };
- console.log('消息查询格式2:', messageQuery2);
- managerSocket.send(JSON.stringify(messageQuery2));
- }
- }, 2000);
-
- // 延迟后尝试格式3
- setTimeout(() => {
- if (!testResults.messageCenterFunctionality) {
- const messageQuery3 = {
- cmd: 'get_chat_list',
- managerId: managerData.userId
- };
- console.log('消息查询格式3:', messageQuery3);
- managerSocket.send(JSON.stringify(messageQuery3));
- }
- }, 4000);
- }, 10000);
-
- // 主动检查在线状态
- setTimeout(() => {
- console.log('🔍 主动检查客服在线状态...');
- managerSocket.send(JSON.stringify({
- type: 'check_online',
- userId: managerData.userId
- }));
- }, 10000);
-
-
-// 运行完整测试
-runChatFunctionalityTests();
diff --git a/update_product_table.js b/update_product_table.js
deleted file mode 100644
index b17d669..0000000
--- a/update_product_table.js
+++ /dev/null
@@ -1,75 +0,0 @@
-// 更新products表结构,添加联系人相关字段
-const mysql = require('mysql2/promise');
-
-async function updateProductTable() {
- let connection;
- try {
- // 连接数据库 - 使用正确的密码
- connection = await mysql.createConnection({
- host: '1.95.162.61',
- port: 3306,
- user: 'root',
- password: 'schl@2025', // 从.env文件中获取的密码
- database: 'wechat_app'
- });
- console.log('✅ 数据库连接成功');
-
- // 检查product_contact字段是否存在
- const [rows] = await connection.query(
- "SELECT column_name FROM information_schema.columns WHERE table_schema = 'wechat_app' AND table_name = 'products' AND column_name = 'product_contact'"
- );
-
- if (rows.length === 0) {
- // 添加product_contact字段
- await connection.query("ALTER TABLE products ADD COLUMN product_contact VARCHAR(100) DEFAULT ''");
- console.log('✅ 已添加product_contact字段');
- } else {
- console.log('ℹ️ product_contact字段已存在');
- }
-
- // 检查contact_phone字段是否存在
- const [phoneRows] = await connection.query(
- "SELECT column_name FROM information_schema.columns WHERE table_schema = 'wechat_app' AND table_name = 'products' AND column_name = 'contact_phone'"
- );
-
- if (phoneRows.length === 0) {
- // 添加contact_phone字段
- await connection.query("ALTER TABLE products ADD COLUMN contact_phone VARCHAR(20) DEFAULT ''");
- console.log('✅ 已添加contact_phone字段');
- } else {
- console.log('ℹ️ contact_phone字段已存在');
- }
-
- // 查询所有已发布商品的数量
- const [productRows] = await connection.query(
- "SELECT COUNT(*) as count FROM products WHERE status = 'published'"
- );
- console.log(`📊 已发布商品数量: ${productRows[0].count}`);
-
- // 查询需要更新联系人信息的商品数量
- const [pendingRows] = await connection.query(
- "SELECT COUNT(*) as count FROM products WHERE status = 'published' AND (product_contact = '' OR product_contact IS NULL OR contact_phone = '' OR contact_phone IS NULL)"
- );
- console.log(`⚠️ 需要更新联系人信息的商品数量: ${pendingRows[0].count}`);
-
- // 显示一些商品数据作为示例
- const [sampleProducts] = await connection.query(
- "SELECT productId, productName, product_contact, contact_phone FROM products WHERE status = 'published' LIMIT 5"
- );
- console.log('\n📋 示例商品数据:');
- sampleProducts.forEach(product => {
- console.log(`- ${product.productName}: 联系人=${product.product_contact || '空'}, 电话=${product.contact_phone || '空'}`);
- });
-
- } catch (error) {
- console.error('❌ 操作失败:', error.message);
- } finally {
- if (connection) {
- await connection.end();
- console.log('\n✅ 数据库连接已关闭');
- }
- }
-}
-
-// 执行更新
-updateProductTable();
From 9ce1f2259dee75b4f8494c42d736f63db2403f1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=A3=9E=E6=B4=8B?=
<15778543+xufeiyang6017@user.noreply.gitee.com>
Date: Wed, 17 Dec 2025 14:48:42 +0800
Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=94=B6=E8=97=8F?=
=?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=BB=BF=E5=A3=B3=E5=95=86=E5=93=81=E5=9B=BE?=
=?UTF-8?q?=E7=89=87=E4=B8=A2=E5=A4=B1=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9F=A5=E8=AF=A2=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5=E5=92=8C=E5=9B=BE=E7=89=87URL=E5=A4=84=E7=90=86?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages/favorites/index.js | 366 +++++++++++++++++++++++++++++++++
pages/favorites/index.json | 4 +
pages/favorites/index.wxml | 114 ++++++++++
pages/favorites/index.wxss | 36 ++++
server-example/server-mysql.js | 231 +++++++++++++++++++++
utils/api.js | 320 +++++++++++++++++++++++++++-
6 files changed, 1066 insertions(+), 5 deletions(-)
create mode 100644 pages/favorites/index.js
create mode 100644 pages/favorites/index.json
create mode 100644 pages/favorites/index.wxml
create mode 100644 pages/favorites/index.wxss
diff --git a/pages/favorites/index.js b/pages/favorites/index.js
new file mode 100644
index 0000000..62415d0
--- /dev/null
+++ b/pages/favorites/index.js
@@ -0,0 +1,366 @@
+// pages/favorites/index.js
+const API = require('../../utils/api.js');
+
+Page({
+
+ /**
+ * 页面的初始数据
+ */
+ data: {
+ favoritesList: [],
+ loading: true,
+ hasFavorites: false,
+
+ // 图片预览相关状态
+ showImagePreview: false, // 控制图片预览弹窗显示
+ previewImageUrls: [], // 预览的图片URL列表
+ previewImageIndex: 0, // 当前预览图片的索引
+
+ // 图片缩放相关状态
+ scale: 1, // 当前缩放比例
+ lastScale: 1, // 上一次缩放比例
+ startDistance: 0, // 双指起始距离
+ doubleTapTimer: null, // 双击计时器
+ lastTapTime: 0, // 上一次单击时间
+ isScaling: false, // 是否正在缩放中
+ offsetX: 0, // X轴偏移量
+ offsetY: 0, // Y轴偏移量
+ initialTouch: null // 初始触摸点
+ },
+
+ /**
+ * 生命周期函数--监听页面加载
+ */
+ onLoad(options) {
+ this.loadFavorites();
+ },
+
+ /**
+ * 生命周期函数--监听页面初次渲染完成
+ */
+ onReady() {
+
+ },
+
+ /**
+ * 生命周期函数--监听页面显示
+ */
+ onShow() {
+ // 每次显示页面时重新加载收藏列表
+ this.loadFavorites();
+ },
+
+ /**
+ * 生命周期函数--监听页面隐藏
+ */
+ onHide() {
+
+ },
+
+ /**
+ * 生命周期函数--监听页面卸载
+ */
+ onUnload() {
+
+ },
+
+ /**
+ * 页面相关事件处理函数--监听用户下拉动作
+ */
+ onPullDownRefresh() {
+ // 下拉刷新时重新加载收藏列表
+ this.loadFavorites(true);
+ },
+
+ /**
+ * 页面上拉触底事件的处理函数
+ */
+ onReachBottom() {
+
+ },
+
+ /**
+ * 用户点击右上角分享
+ */
+ onShareAppMessage() {
+
+ },
+
+ /**
+ * 加载收藏列表
+ */
+ loadFavorites: function (isPullDown = false) {
+ this.setData({
+ loading: true
+ });
+
+ // 获取手机号码
+ const phoneNumber = wx.getStorageSync('phoneNumber') || '18482694520'; // 默认使用测试手机号
+
+ if (!phoneNumber) {
+ // 用户未登录,显示提示
+ wx.showToast({
+ title: '请先登录',
+ icon: 'none'
+ });
+ this.setData({
+ loading: false,
+ hasFavorites: false
+ });
+ if (isPullDown) {
+ wx.stopPullDownRefresh();
+ }
+ return;
+ }
+
+ API.getFavorites(phoneNumber).then(res => {
+ console.log('获取收藏列表成功:', res);
+ const favorites = res.data && res.data.favorites ? res.data.favorites : [];
+ this.setData({
+ favoritesList: favorites,
+ hasFavorites: favorites.length > 0,
+ loading: false
+ });
+ }).catch(err => {
+ console.error('获取收藏列表失败:', err);
+ wx.showToast({
+ title: '获取收藏列表失败',
+ icon: 'none'
+ });
+ this.setData({
+ loading: false,
+ hasFavorites: false
+ });
+ }).finally(() => {
+ // 停止下拉刷新
+ if (isPullDown) {
+ wx.stopPullDownRefresh();
+ }
+ });
+ },
+
+ /**
+ * 取消收藏
+ */
+ cancelFavorite: function (e) {
+ const productId = e.currentTarget.dataset.productid;
+ wx.showLoading({
+ title: '正在取消收藏',
+ mask: true
+ });
+
+ API.cancelFavorite(productId).then(res => {
+ console.log('取消收藏成功:', res);
+ // 从收藏列表中移除该商品
+ const updatedList = this.data.favoritesList.filter(item => item.productId !== productId);
+ this.setData({
+ favoritesList: updatedList,
+ hasFavorites: updatedList.length > 0
+ });
+ wx.showToast({
+ title: '取消收藏成功',
+ icon: 'success'
+ });
+ }).catch(err => {
+ console.error('取消收藏失败:', err);
+ wx.showToast({
+ title: '取消收藏失败',
+ icon: 'none'
+ });
+ }).finally(() => {
+ wx.hideLoading();
+ });
+ },
+
+ /**
+ * 跳转到商品详情页
+ */
+ goToGoodsDetail: function (e) {
+ const productId = e.currentTarget.dataset.productid;
+ wx.navigateTo({
+ url: '/pages/goods-detail/goods-detail?productId=' + productId
+ });
+ },
+
+ // 轮播图切换
+ swiperChange(e) {
+ const current = e.detail.current;
+ const itemId = e.currentTarget.dataset.itemId;
+
+ // 更新对应商品项的currentImageIndex
+ this.setData({
+ [`favoritesList[${itemId}].currentImageIndex`]: current
+ });
+ },
+
+ // 预览图片
+ previewImage(e) {
+ // 登录验证
+ const userInfo = wx.getStorageSync('userInfo') || null;
+ const userId = wx.getStorageSync('userId') || null;
+ if (!userInfo || !userId) {
+ // 未登录,显示授权登录弹窗
+ this.setData({
+ showAuthModal: true,
+ pendingUserType: 'buyer'
+ });
+ return;
+ }
+
+ // 已登录,执行图片预览
+ const { urls, index } = e.currentTarget.dataset;
+ this.setData({
+ showImagePreview: true,
+ previewImageUrls: urls,
+ previewImageIndex: parseInt(index)
+ });
+ },
+
+ // 关闭图片预览
+ closeImagePreview() {
+ this.setData({
+ showImagePreview: false
+ });
+ this.resetZoom();
+ },
+
+ // 重置缩放状态
+ resetZoom() {
+ this.setData({
+ scale: 1,
+ lastScale: 1,
+ offsetX: 0,
+ offsetY: 0,
+ initialTouch: null
+ });
+ },
+
+ // 处理图片点击事件(单击/双击判断)
+ handleImageTap(e) {
+ const currentTime = Date.now();
+ const lastTapTime = this.data.lastTapTime;
+
+ // 判断是否为双击(300ms内连续点击)
+ if (currentTime - lastTapTime < 300) {
+ // 双击事件
+ if (this.data.doubleTapTimer) {
+ clearTimeout(this.data.doubleTapTimer);
+ }
+
+ // 切换放大/缩小状态
+ const newScale = this.data.scale === 1 ? 2 : 1;
+ this.setData({
+ scale: newScale,
+ lastScale: newScale,
+ offsetX: 0,
+ offsetY: 0,
+ lastTapTime: 0 // 重置双击状态
+ });
+ } else {
+ // 单击事件,设置延迟来检测是否会成为双击
+ if (this.data.doubleTapTimer) {
+ clearTimeout(this.data.doubleTapTimer);
+ }
+
+ this.setData({
+ lastTapTime: currentTime,
+ doubleTapTimer: setTimeout(() => {
+ // 确认是单击,关闭图片预览
+ this.closeImagePreview();
+ }, 300)
+ });
+ }
+ },
+
+ // 处理触摸开始事件
+ handleTouchStart(e) {
+ const touches = e.touches;
+
+ if (touches.length === 1) {
+ // 单指:准备拖动
+ this.setData({
+ initialTouch: {
+ x: touches[0].clientX,
+ y: touches[0].clientY
+ }
+ });
+ } else if (touches.length === 2) {
+ // 双指:记录起始距离,准备缩放
+ const distance = this.calculateDistance(touches[0], touches[1]);
+ this.setData({
+ startDistance: distance,
+ isScaling: true,
+ lastScale: this.data.scale
+ });
+ }
+ },
+
+ // 处理触摸移动事件
+ handleTouchMove(e) {
+ const touches = e.touches;
+
+ if (touches.length === 1 && this.data.initialTouch && this.data.scale !== 1) {
+ // 单指拖动(只有在缩放状态下才允许拖动)
+ const deltaX = touches[0].clientX - this.data.initialTouch.x;
+ const deltaY = touches[0].clientY - this.data.initialTouch.y;
+
+ // 计算新的偏移量
+ let newOffsetX = this.data.offsetX + deltaX;
+ let newOffsetY = this.data.offsetY + deltaY;
+
+ // 边界限制
+ const windowWidth = wx.getSystemInfoSync().windowWidth;
+ const windowHeight = wx.getSystemInfoSync().windowHeight;
+ const maxOffsetX = (windowWidth * (this.data.scale - 1)) / 2;
+ const maxOffsetY = (windowHeight * (this.data.scale - 1)) / 2;
+
+ newOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
+ newOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
+
+ this.setData({
+ offsetX: newOffsetX,
+ offsetY: newOffsetY,
+ initialTouch: {
+ x: touches[0].clientX,
+ y: touches[0].clientY
+ }
+ });
+ } else if (touches.length === 2) {
+ // 双指缩放
+ const currentDistance = this.calculateDistance(touches[0], touches[1]);
+ const scale = (currentDistance / this.data.startDistance) * this.data.lastScale;
+
+ // 限制缩放范围在0.5倍到3倍之间
+ const newScale = Math.max(0.5, Math.min(3, scale));
+
+ this.setData({
+ scale: newScale,
+ isScaling: true
+ });
+ }
+ },
+
+ // 处理触摸结束事件
+ handleTouchEnd(e) {
+ this.setData({
+ isScaling: false,
+ lastScale: this.data.scale,
+ initialTouch: null
+ });
+ },
+
+ // 计算两点之间的距离
+ calculateDistance(touch1, touch2) {
+ const dx = touch2.clientX - touch1.clientX;
+ const dy = touch2.clientY - touch1.clientY;
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+
+ // 切换预览图片
+ onPreviewImageChange(e) {
+ this.setData({
+ previewImageIndex: e.detail.current
+ });
+ this.resetZoom();
+ }
+})
\ No newline at end of file
diff --git a/pages/favorites/index.json b/pages/favorites/index.json
new file mode 100644
index 0000000..21582e1
--- /dev/null
+++ b/pages/favorites/index.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationBarTitleText": "我的收藏"
+}
\ No newline at end of file
diff --git a/pages/favorites/index.wxml b/pages/favorites/index.wxml
new file mode 100644
index 0000000..29b1b11
--- /dev/null
+++ b/pages/favorites/index.wxml
@@ -0,0 +1,114 @@
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+ 💔
+ 您还没有收藏任何商品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无图片
+
+
+
+
+
+
+
+
+
+
+
+
+ {{(item.currentImageIndex || 0) + 1}}/{{item.Product.imageUrls.length}}
+
+
+
+
+
+
+
+
+
+
+ 金标蛋
+ {{item.Product.productName || '未命名商品'}}
+
+
+ {{(item.Product.spec && item.Product.spec !== '无') ? item.Product.spec : (item.Product.specification && item.Product.specification !== '无') ? item.Product.specification : '无'}} | {{item.Product.yolk || '无'}} | {{item.Product.minOrder || item.Product.quantity || 1}}件
+
+
+
+
+
+
+
+ ¥{{item.Product.price || 0}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
\ No newline at end of file
diff --git a/pages/favorites/index.wxss b/pages/favorites/index.wxss
new file mode 100644
index 0000000..f8e9f28
--- /dev/null
+++ b/pages/favorites/index.wxss
@@ -0,0 +1,36 @@
+/* pages/favorites/index.wxss */
+.container {
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 50vh;
+}
+
+.empty-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 50vh;
+ color: #999;
+}
+
+.empty-icon {
+ font-size: 100rpx;
+ margin-bottom: 20rpx;
+}
+
+.empty-text {
+ font-size: 28rpx;
+}
+
+/* 图片轮播样式 */
+.image-swiper {
+ width: 100%;
+ height: 100%;
+}
\ No newline at end of file
diff --git a/server-example/server-mysql.js b/server-example/server-mysql.js
index 4b0e284..589696f 100644
--- a/server-example/server-mysql.js
+++ b/server-example/server-mysql.js
@@ -813,6 +813,33 @@ CartItem.init({
timestamps: false
});
+// 收藏模型
+class Favorite extends Model { }
+Favorite.init({
+ id: {
+ type: DataTypes.INTEGER,
+ autoIncrement: true,
+ primaryKey: true
+ },
+ user_phone: {
+ type: DataTypes.STRING(20),
+ allowNull: false
+ },
+ productId: {
+ type: DataTypes.STRING(100),
+ allowNull: false
+ },
+ date: {
+ type: DataTypes.DATE,
+ defaultValue: Sequelize.NOW
+ }
+}, {
+ sequelize,
+ modelName: 'Favorite',
+ tableName: 'favorites',
+ timestamps: false
+});
+
// 联系人表模型
class Contact extends Model { }
Contact.init({
@@ -957,6 +984,21 @@ Product.hasMany(CartItem, {
onUpdate: 'CASCADE' // 级联更新
});
+// 收藏与商品的关系 (收藏属于商品)
+Favorite.belongsTo(Product, {
+ foreignKey: 'productId',
+ targetKey: 'productId', // 目标键,使用productId字段(STRING类型)而非默认的id字段(INTEGER类型)
+ as: 'Product' // 别名,与收藏列表API中使用的一致
+});
+
+// 商品与收藏的一对多关系 (商品可以被多个用户收藏)
+Product.hasMany(Favorite, {
+ foreignKey: 'productId',
+ as: 'favorites', // 别名,表示商品的收藏列表
+ onDelete: 'CASCADE', // 级联删除
+ onUpdate: 'CASCADE' // 级联更新
+});
+
CartItem.belongsTo(Product, {
foreignKey: 'productId',
as: 'product' // 购物车项中的商品
@@ -3250,6 +3292,195 @@ async function handleAddImagesToExistingProduct(req, res, existingProductId, upl
// 其他路由...
+// 收藏相关API端点
+// 添加收藏
+app.post('/api/favorites/add', async (req, res) => {
+ try {
+ const { user_phone, productId } = req.body;
+
+ console.log('收到添加收藏请求:', { user_phone, productId });
+
+ // 验证参数
+ if (!user_phone || !productId) {
+ return res.status(400).json({
+ success: false,
+ code: 400,
+ message: '缺少必要参数',
+ data: { user_phone, productId }
+ });
+ }
+
+ // 检查是否已存在收藏记录
+ const existingFavorite = await Favorite.findOne({
+ where: {
+ user_phone,
+ productId
+ }
+ });
+
+ if (existingFavorite) {
+ return res.status(200).json({
+ success: true,
+ code: 200,
+ message: '已经收藏过该商品',
+ data: existingFavorite
+ });
+ }
+
+ // 创建新的收藏记录
+ const newFavorite = await Favorite.create({
+ user_phone,
+ productId
+ });
+
+ console.log('收藏添加成功:', newFavorite);
+
+ res.status(200).json({
+ success: true,
+ code: 200,
+ message: '收藏添加成功',
+ data: newFavorite
+ });
+ } catch (error) {
+ console.error('添加收藏失败:', error);
+ res.status(500).json({
+ success: false,
+ code: 500,
+ message: '添加收藏失败',
+ error: error.message
+ });
+ }
+});
+
+// 取消收藏
+app.post('/api/favorites/remove', async (req, res) => {
+ try {
+ const { user_phone, productId } = req.body;
+
+ console.log('收到取消收藏请求:', { user_phone, productId });
+
+ // 验证参数
+ if (!user_phone || !productId) {
+ return res.status(400).json({
+ success: false,
+ code: 400,
+ message: '缺少必要参数'
+ });
+ }
+
+ // 删除收藏记录
+ const result = await Favorite.destroy({
+ where: {
+ user_phone,
+ productId
+ }
+ });
+
+ if (result === 0) {
+ return res.status(404).json({
+ success: false,
+ code: 404,
+ message: '收藏记录不存在'
+ });
+ }
+
+ console.log('收藏取消成功:', { user_phone, productId });
+
+ res.status(200).json({
+ success: true,
+ code: 200,
+ message: '收藏取消成功'
+ });
+ } catch (error) {
+ console.error('取消收藏失败:', error);
+ res.status(500).json({
+ success: false,
+ code: 500,
+ message: '取消收藏失败',
+ error: error.message
+ });
+ }
+});
+
+// 获取收藏列表
+app.post('/api/favorites/list', async (req, res) => {
+ try {
+ const { user_phone } = req.body;
+
+ console.log('收到获取收藏列表请求:', { user_phone });
+
+ // 验证参数
+ if (!user_phone) {
+ return res.status(400).json({
+ success: false,
+ code: 400,
+ message: '缺少必要参数'
+ });
+ }
+
+ // 获取收藏列表,并关联查询商品信息
+ const favorites = await Favorite.findAll({
+ where: { user_phone },
+ include: [
+ {
+ model: Product,
+ as: 'Product', // 与关联定义中的别名一致
+ attributes: ['productId', 'productName', 'price', 'quantity', 'grossWeight', 'imageUrls', 'created_at', 'specification', 'yolk']
+ }
+ ],
+ order: [['date', 'DESC']]
+ });
+
+ console.log('获取收藏列表成功,数量:', favorites.length);
+
+ // 处理图片URL,确保是数组格式(与商品详情API保持一致的处理逻辑)
+ const processedFavorites = favorites.map(favorite => {
+ const favoriteJSON = favorite.toJSON();
+ if (favoriteJSON.Product) {
+ // 关键修复:将存储在数据库中的JSON字符串反序列化为JavaScript数组
+ if (favoriteJSON.Product.imageUrls) {
+ if (typeof favoriteJSON.Product.imageUrls === 'string') {
+ console.log('【关键修复】将数据库中的JSON字符串反序列化为JavaScript数组');
+ try {
+ favoriteJSON.Product.imageUrls = JSON.parse(favoriteJSON.Product.imageUrls);
+ console.log('反序列化后的imageUrls类型:', typeof favoriteJSON.Product.imageUrls);
+ } catch (parseError) {
+ console.error('反序列化imageUrls失败:', parseError);
+ // 如果解析失败,使用空数组确保前端不会崩溃
+ favoriteJSON.Product.imageUrls = [];
+ }
+ } else if (!Array.isArray(favoriteJSON.Product.imageUrls)) {
+ // 如果不是数组类型,也转换为空数组
+ favoriteJSON.Product.imageUrls = [];
+ }
+ } else {
+ // 确保imageUrls始终是数组
+ favoriteJSON.Product.imageUrls = [];
+ }
+ }
+ return favoriteJSON;
+ });
+
+ res.status(200).json({
+ success: true,
+ code: 200,
+ message: '获取收藏列表成功',
+ data: {
+ favorites: processedFavorites,
+ count: processedFavorites.length
+ }
+ });
+ } catch (error) {
+ console.error('获取收藏列表失败:', error);
+ res.status(500).json({
+ success: false,
+ code: 500,
+ message: '获取收藏列表失败',
+ error: error.message
+ });
+ }
+});
+
// 辅助函数:清理临时文件
function cleanTempFiles(filePaths) {
if (!filePaths || filePaths.length === 0) {
diff --git a/utils/api.js b/utils/api.js
index e06d9a3..8356359 100644
--- a/utils/api.js
+++ b/utils/api.js
@@ -1998,20 +1998,20 @@ module.exports = {
// 清除过期的登录信息
try {
wx.removeStorageSync('openid');
+ wx.removeStorageSync('userId');
wx.removeStorageSync('sessionKey');
} catch (e) {
console.error('清除过期登录信息失败:', e);
}
// 重新登录后重试
- return this.login().then(() => {
- console.log('重新登录成功,准备重试上传手机号数据');
+ return this.login().then(loginRes => {
return tryUpload();
});
+ } else {
+ // 其他错误或重试次数用完,直接抛出
+ throw error;
}
-
- // 其他错误直接抛出
- throw error;
});
};
@@ -2019,6 +2019,231 @@ module.exports = {
return tryUpload();
},
+ // 添加收藏
+ addFavorite: function (productId) {
+ console.log('API.addFavorite - productId:', productId);
+ return new Promise((resolve, reject) => {
+ const openid = wx.getStorageSync('openid');
+ const userId = wx.getStorageSync('userId');
+
+ // 获取用户信息,包含手机号
+ const users = wx.getStorageSync('users') || {};
+ let userPhone = null;
+
+ // 尝试从users中获取手机号
+ if (userId && users[userId] && users[userId].phoneNumber) {
+ userPhone = users[userId].phoneNumber;
+ } else {
+ // 尝试从全局用户信息获取
+ const userInfo = wx.getStorageSync('userInfo');
+ if (userInfo && userInfo.phoneNumber) {
+ userPhone = userInfo.phoneNumber;
+ }
+ }
+
+ // 如果没有openid,需要先登录
+ if (!openid) {
+ return this.login().then(loginRes => {
+ // 重新尝试添加收藏
+ return this.addFavorite(productId);
+ }).catch(loginErr => {
+ reject(new Error('用户未登录,无法添加收藏'));
+ });
+ }
+
+ // 如果没有手机号,需要先获取
+ if (!userPhone) {
+ // 尝试获取用户信息
+ return this.getUserInfo(openid).then(userInfoRes => {
+ let phoneNumber = null;
+ if (userInfoRes.data && userInfoRes.data.phoneNumber) {
+ phoneNumber = userInfoRes.data.phoneNumber;
+ }
+
+ if (!phoneNumber) {
+ reject(new Error('无法获取用户手机号,无法添加收藏'));
+ } else {
+ // 保存手机号到本地
+ if (userId) {
+ users[userId] = users[userId] || {};
+ users[userId].phoneNumber = phoneNumber;
+ wx.setStorageSync('users', users);
+ }
+ // 重新尝试添加收藏
+ return this.addFavorite(productId);
+ }
+ }).catch(err => {
+ reject(new Error('无法获取用户信息,无法添加收藏'));
+ });
+ }
+
+ // 构建收藏数据
+ const favoriteData = {
+ user_phone: userPhone,
+ productId: productId,
+ date: new Date().toISOString() // 当前时间
+ };
+
+ console.log('添加收藏请求数据:', favoriteData);
+
+ // 发送请求到服务器
+ request('/api/favorites/add', 'POST', favoriteData).then(res => {
+ console.log('添加收藏成功:', res);
+ resolve(res);
+ }).catch(error => {
+ console.error('添加收藏失败:', error);
+ reject(new Error('添加收藏失败,请稍后重试'));
+ });
+ });
+ },
+
+ // 取消收藏
+ cancelFavorite: function (productId) {
+ console.log('API.cancelFavorite - productId:', productId);
+ return new Promise((resolve, reject) => {
+ const openid = wx.getStorageSync('openid');
+
+ // 获取用户信息,包含手机号
+ const users = wx.getStorageSync('users') || {};
+ const userId = wx.getStorageSync('userId');
+ let userPhone = null;
+
+ // 尝试从users中获取手机号
+ if (userId && users[userId] && users[userId].phoneNumber) {
+ userPhone = users[userId].phoneNumber;
+ } else {
+ // 尝试从全局用户信息获取
+ const userInfo = wx.getStorageSync('userInfo');
+ if (userInfo && userInfo.phoneNumber) {
+ userPhone = userInfo.phoneNumber;
+ }
+ }
+
+ if (!openid) {
+ return this.login().then(loginRes => {
+ // 重新尝试取消收藏
+ return this.cancelFavorite(productId);
+ }).catch(loginErr => {
+ reject(new Error('用户未登录,无法取消收藏'));
+ });
+ }
+
+ // 如果没有手机号,需要先获取
+ if (!userPhone) {
+ // 尝试获取用户信息
+ return this.getUserInfo(openid).then(userInfoRes => {
+ let phoneNumber = null;
+ if (userInfoRes.data && userInfoRes.data.phoneNumber) {
+ phoneNumber = userInfoRes.data.phoneNumber;
+ }
+
+ if (!phoneNumber) {
+ reject(new Error('无法获取用户手机号,无法取消收藏'));
+ } else {
+ // 保存手机号到本地
+ if (userId) {
+ users[userId] = users[userId] || {};
+ users[userId].phoneNumber = phoneNumber;
+ wx.setStorageSync('users', users);
+ }
+ // 重新尝试取消收藏
+ return this.cancelFavorite(productId);
+ }
+ }).catch(err => {
+ reject(new Error('无法获取用户信息,无法取消收藏'));
+ });
+ }
+
+ const cancelData = {
+ user_phone: userPhone,
+ productId: productId
+ };
+
+ console.log('取消收藏请求数据:', cancelData);
+
+ request('/api/favorites/cancel', 'POST', cancelData).then(res => {
+ console.log('取消收藏成功:', res);
+ resolve(res);
+ }).catch(error => {
+ console.error('取消收藏失败:', error);
+ reject(new Error('取消收藏失败,请稍后重试'));
+ });
+ });
+ },
+
+ // 获取用户收藏列表
+ getFavorites: function () {
+ console.log('API.getFavorites');
+ return new Promise((resolve, reject) => {
+ const openid = wx.getStorageSync('openid');
+
+ // 获取用户信息,包含手机号
+ const users = wx.getStorageSync('users') || {};
+ const userId = wx.getStorageSync('userId');
+ let userPhone = null;
+
+ // 尝试从users中获取手机号
+ if (userId && users[userId] && users[userId].phoneNumber) {
+ userPhone = users[userId].phoneNumber;
+ } else {
+ // 尝试从全局用户信息获取
+ const userInfo = wx.getStorageSync('userInfo');
+ if (userInfo && userInfo.phoneNumber) {
+ userPhone = userInfo.phoneNumber;
+ }
+ }
+
+ if (!openid) {
+ return this.login().then(loginRes => {
+ // 重新尝试获取收藏列表
+ return this.getFavorites();
+ }).catch(loginErr => {
+ reject(new Error('用户未登录,无法获取收藏列表'));
+ });
+ }
+
+ // 如果没有手机号,需要先获取
+ if (!userPhone) {
+ // 尝试获取用户信息
+ return this.getUserInfo(openid).then(userInfoRes => {
+ let phoneNumber = null;
+ if (userInfoRes.data && userInfoRes.data.phoneNumber) {
+ phoneNumber = userInfoRes.data.phoneNumber;
+ }
+
+ if (!phoneNumber) {
+ reject(new Error('无法获取用户手机号,无法获取收藏列表'));
+ } else {
+ // 保存手机号到本地
+ if (userId) {
+ users[userId] = users[userId] || {};
+ users[userId].phoneNumber = phoneNumber;
+ wx.setStorageSync('users', users);
+ }
+ // 重新尝试获取收藏列表
+ return this.getFavorites();
+ }
+ }).catch(err => {
+ reject(new Error('无法获取用户信息,无法获取收藏列表'));
+ });
+ }
+
+ const requestData = {
+ user_phone: userPhone
+ };
+
+ console.log('获取收藏列表请求数据:', requestData);
+
+ request('/api/favorites/list', 'GET', requestData).then(res => {
+ console.log('获取收藏列表成功:', res);
+ resolve(res);
+ }).catch(error => {
+ console.error('获取收藏列表失败:', error);
+ reject(new Error('获取收藏列表失败,请稍后重试'));
+ });
+ });
+ },
+
// 上传用户信息到服务器
uploadUserInfo: function (userInfo) {
console.log('API.uploadUserInfo - userInfo:', userInfo);
@@ -2633,5 +2858,90 @@ module.exports = {
});
},
+ // 获取收藏商品列表
+ getFavorites: function (phoneNumber) {
+ console.log('API.getFavorites - phoneNumber:', phoneNumber);
+ if (!phoneNumber) {
+ return Promise.reject(new Error('用户未登录'));
+ }
+
+ return request('/api/favorites/list', 'POST', {
+ user_phone: phoneNumber
+ });
+ },
+
+ // 取消收藏商品
+ cancelFavorite: function (productId) {
+ console.log('API.cancelFavorite - productId:', productId);
+ return new Promise((resolve, reject) => {
+ const openid = wx.getStorageSync('openid');
+
+ // 获取用户信息,包含手机号
+ const users = wx.getStorageSync('users') || {};
+ const userId = wx.getStorageSync('userId');
+ let userPhone = null;
+
+ // 尝试从users中获取手机号
+ if (userId && users[userId] && users[userId].phoneNumber) {
+ userPhone = users[userId].phoneNumber;
+ } else {
+ // 尝试从全局用户信息获取
+ const userInfo = wx.getStorageSync('userInfo');
+ if (userInfo && userInfo.phoneNumber) {
+ userPhone = userInfo.phoneNumber;
+ }
+ }
+
+ if (!openid) {
+ return this.login().then(loginRes => {
+ // 重新尝试取消收藏
+ return this.cancelFavorite(productId);
+ }).catch(loginErr => {
+ reject(new Error('用户未登录,无法取消收藏'));
+ });
+ }
+
+ // 如果没有手机号,需要先获取
+ if (!userPhone) {
+ // 尝试获取用户信息
+ return this.getUserInfo(openid).then(userInfoRes => {
+ let phoneNumber = null;
+ if (userInfoRes.data && userInfoRes.data.phoneNumber) {
+ phoneNumber = userInfoRes.data.phoneNumber;
+ }
+
+ if (!phoneNumber) {
+ reject(new Error('无法获取用户手机号,无法取消收藏'));
+ } else {
+ // 保存手机号到本地
+ if (userId) {
+ users[userId] = users[userId] || {};
+ users[userId].phoneNumber = phoneNumber;
+ wx.setStorageSync('users', users);
+ }
+ // 重新尝试取消收藏
+ return this.cancelFavorite(productId);
+ }
+ }).catch(err => {
+ reject(new Error('无法获取用户信息,无法取消收藏'));
+ });
+ }
+
+ const requestData = {
+ user_phone: userPhone,
+ productId: productId
+ };
+
+ console.log('取消收藏请求数据:', requestData);
+
+ request('/api/favorites/remove', 'POST', requestData).then(res => {
+ console.log('取消收藏成功:', res);
+ resolve(res);
+ }).catch(error => {
+ console.error('取消收藏失败:', error);
+ reject(new Error('取消收藏失败,请稍后重试'));
+ });
+ });
+ }
};
\ No newline at end of file
From d8dca30c9a96be3f381ba2e0e42311e9ce46479d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=A3=9E=E6=B4=8B?=
<15778543+xufeiyang6017@user.noreply.gitee.com>
Date: Wed, 17 Dec 2025 15:11:16 +0800
Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=94=B6=E8=97=8F?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F?=
=?UTF-8?q?=E6=8C=89=E9=92=AE=E3=80=81=E6=94=B6=E8=97=8F=E4=BF=A1=E6=81=AF?=
=?UTF-8?q?=E6=98=BE=E7=A4=BA=E3=80=81=E7=BB=BF=E5=A3=B3=E4=BA=A7=E5=93=81?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=A2=E5=A4=B1=E3=80=81=E9=87=8D=E5=A4=8D?=
=?UTF-8?q?=E6=94=B6=E8=97=8F=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app.json | 1 +
custom-tab-bar/index.js | 71 +-
custom-tab-bar/index.wxml | 10 +-
how origin | 14 -
images/生成鸡蛋贸易平台图片.png | Bin 0 -> 1255603 bytes
page.html | 480 -------
pages/buyer/index.js | 223 ++-
pages/buyer/index.wxml | 69 +-
pages/chat-detail/system-test.js | 255 ++++
pages/chat-detail/test-backup-query.js | 158 ++
pages/chat-detail/test-real-user.js | 192 +++
pages/test-service/test-service.json | 3 +
.../check-chat-online-status-detailed.js | 108 ++
server-example/check-online-status.js | 70 +
server-example/check_messages_in_db.js | 220 +++
server-example/check_specific_conversation.js | 81 ++
server-example/cleanup_invalid_chat_data.js | 76 +
server-example/cleanup_temp_user_ids.js | 107 ++
server-example/cleanup_test_data.js | 29 +
.../comprehensive-manager-status-test.js | 323 +++++
server-example/comprehensive_chat_test.js | 245 ++++
.../customer-service-status-test.js | 75 +
server-example/debug-websocket.js | 116 ++
server-example/debug_chat_reply.js | 121 ++
server-example/debug_complete_flow.js | 193 +++
server-example/debug_final.js | 112 ++
server-example/debug_full_flow.js | 215 +++
server-example/debug_log_server.js | 86 ++
server-example/debug_manager_message.js | 138 ++
server-example/debug_new_conversation.js | 244 ++++
server-example/debug_simple.js | 111 ++
server-example/debug_verbose.js | 191 +++
server-example/diagnose-customer-service.js | 252 ++++
server-example/fix_chat_functionality.js | 236 +++
server-example/minimal_message_test.js | 81 ++
.../simple-customer-service-test.js | 125 ++
server-example/simple_message_center_test.js | 142 ++
server-example/simple_verify.js | 110 ++
.../test-customer-service-online.js | 134 ++
server-example/test-manager-online-check.js | 111 ++
server-example/test-new-auth-mechanism.js | 360 +++++
server-example/test-type-sync-fix.js | 166 +++
server-example/test-user-auth-validation.js | 85 ++
server-example/test_chat_flow.js | 85 ++
.../test_chat_functionality_complete.js | 215 +++
server-example/test_complete_chat_flow.js | 279 ++++
server-example/test_correct_message_format.js | 193 +++
.../test_improved_message_center.js | 247 ++++
server-example/test_manager_conversations.js | 39 +
server-example/test_message_center.js | 215 +++
server-example/test_message_issue.js | 108 ++
.../test_specific_customer_service.js | 345 +++++
.../verify-customer-service-online.js | 168 +++
.../verify-manager-online-complete.js | 112 ++
server-example/verify_chat_fix.js | 83 ++
server-example/verify_message_fix.js | 86 ++
.../聊天功能实现逻辑分析文档.md | 333 +++++
simple_chat_test.js | 138 --
test_chat_connection.js | 96 --
test_chat_functionality.js | 1276 -----------------
utils/auth.js | 186 +++
61 files changed, 8266 insertions(+), 2077 deletions(-)
delete mode 100644 how origin
create mode 100644 images/生成鸡蛋贸易平台图片.png
delete mode 100644 page.html
create mode 100644 pages/chat-detail/system-test.js
create mode 100644 pages/chat-detail/test-backup-query.js
create mode 100644 pages/chat-detail/test-real-user.js
create mode 100644 pages/test-service/test-service.json
create mode 100644 server-example/check-chat-online-status-detailed.js
create mode 100644 server-example/check-online-status.js
create mode 100644 server-example/check_messages_in_db.js
create mode 100644 server-example/check_specific_conversation.js
create mode 100644 server-example/cleanup_invalid_chat_data.js
create mode 100644 server-example/cleanup_temp_user_ids.js
create mode 100644 server-example/cleanup_test_data.js
create mode 100644 server-example/comprehensive-manager-status-test.js
create mode 100644 server-example/comprehensive_chat_test.js
create mode 100644 server-example/customer-service-status-test.js
create mode 100644 server-example/debug-websocket.js
create mode 100644 server-example/debug_chat_reply.js
create mode 100644 server-example/debug_complete_flow.js
create mode 100644 server-example/debug_final.js
create mode 100644 server-example/debug_full_flow.js
create mode 100644 server-example/debug_log_server.js
create mode 100644 server-example/debug_manager_message.js
create mode 100644 server-example/debug_new_conversation.js
create mode 100644 server-example/debug_simple.js
create mode 100644 server-example/debug_verbose.js
create mode 100644 server-example/diagnose-customer-service.js
create mode 100644 server-example/fix_chat_functionality.js
create mode 100644 server-example/minimal_message_test.js
create mode 100644 server-example/simple-customer-service-test.js
create mode 100644 server-example/simple_message_center_test.js
create mode 100644 server-example/simple_verify.js
create mode 100644 server-example/test-customer-service-online.js
create mode 100644 server-example/test-manager-online-check.js
create mode 100644 server-example/test-new-auth-mechanism.js
create mode 100644 server-example/test-type-sync-fix.js
create mode 100644 server-example/test-user-auth-validation.js
create mode 100644 server-example/test_chat_flow.js
create mode 100644 server-example/test_chat_functionality_complete.js
create mode 100644 server-example/test_complete_chat_flow.js
create mode 100644 server-example/test_correct_message_format.js
create mode 100644 server-example/test_improved_message_center.js
create mode 100644 server-example/test_manager_conversations.js
create mode 100644 server-example/test_message_center.js
create mode 100644 server-example/test_message_issue.js
create mode 100644 server-example/test_specific_customer_service.js
create mode 100644 server-example/verify-customer-service-online.js
create mode 100644 server-example/verify-manager-online-complete.js
create mode 100644 server-example/verify_chat_fix.js
create mode 100644 server-example/verify_message_fix.js
create mode 100644 server-example/聊天功能实现逻辑分析文档.md
delete mode 100644 simple_chat_test.js
delete mode 100644 test_chat_connection.js
delete mode 100644 test_chat_functionality.js
create mode 100644 utils/auth.js
diff --git a/app.json b/app.json
index d42d042..2b79313 100644
--- a/app.json
+++ b/app.json
@@ -7,6 +7,7 @@
"pages/buyer/index",
"pages/seller/index",
"pages/profile/index",
+ "pages/favorites/index",
"pages/notopen/index",
"pages/create-supply/index",
"pages/goods-detail/goods-detail",
diff --git a/custom-tab-bar/index.js b/custom-tab-bar/index.js
index 0c27e46..9c683d1 100644
--- a/custom-tab-bar/index.js
+++ b/custom-tab-bar/index.js
@@ -16,7 +16,7 @@ Component({
tabBarItems: [
{ key: 'index', route: 'pages/index/index' },
{ key: 'buyer', route: 'pages/buyer/index', badgeKey: 'chat' }, // 聊天功能可能在buyer tab
- { key: 'seller', route: 'pages/seller/index' },
+ { key: 'favorites', route: 'pages/favorites/index' },
{ key: 'profile', route: 'pages/profile/index' }
]
},
@@ -89,26 +89,49 @@ Component({
// 跳转到tab页面的通用方法
navigateToTabPage(url) {
- console.log('使用switchTab跳转到tabbar页面:', url)
- wx.switchTab({
- url: '/' + url,
- success: (res) => {
- console.log('switchTab成功:', url, res)
- },
- fail: (err) => {
- console.error('switchTab失败:', url, err)
- console.log('尝试使用reLaunch跳转...')
- wx.reLaunch({
- url: '/' + url,
- success: (res) => {
- console.log('reLaunch成功:', url, res)
- },
- fail: (err) => {
- console.error('reLaunch也失败:', url, err)
- }
- })
- }
- })
+ // 定义tabBar页面列表
+ const tabBarPages = [
+ 'pages/index/index',
+ 'pages/buyer/index',
+ 'pages/seller/index',
+ 'pages/profile/index'
+ ];
+
+ // 检查是否为tabBar页面
+ if (tabBarPages.includes(url)) {
+ console.log('使用switchTab跳转到tabbar页面:', url)
+ wx.switchTab({
+ url: '/' + url,
+ success: (res) => {
+ console.log('switchTab成功:', url, res)
+ },
+ fail: (err) => {
+ console.error('switchTab失败:', url, err)
+ console.log('尝试使用reLaunch跳转...')
+ wx.reLaunch({
+ url: '/' + url,
+ success: (res) => {
+ console.log('reLaunch成功:', url, res)
+ },
+ fail: (err) => {
+ console.error('reLaunch也失败:', url, err)
+ }
+ })
+ }
+ })
+ } else {
+ // 非tabBar页面,使用navigateTo跳转
+ console.log('使用navigateTo跳转到非tabbar页面:', url)
+ wx.navigateTo({
+ url: '/' + url,
+ success: (res) => {
+ console.log('navigateTo成功:', url, res)
+ },
+ fail: (err) => {
+ console.error('navigateTo失败:', url, err)
+ }
+ })
+ }
},
// 强制更新选中状态
@@ -143,10 +166,10 @@ Component({
}
},
- // 跳转到鸡蛋估价页面 - 现已改为未开放页面
- goToEvaluatePage() {
+ // 跳转到收藏页面
+ goToFavoritesPage() {
wx.navigateTo({
- url: '/pages/evaluate/index'
+ url: '/pages/favorites/index'
})
},
diff --git a/custom-tab-bar/index.wxml b/custom-tab-bar/index.wxml
index df49aad..9db4045 100644
--- a/custom-tab-bar/index.wxml
+++ b/custom-tab-bar/index.wxml
@@ -32,14 +32,14 @@
-
- {{badges['seller']}}
+ {{badges['favorites']}}
- 卖蛋
+ 收藏
(xqp?iiRC{FsNlfMnNHv?z2cBgCwnwGTc>CYxQpEs$DE%iFPc0jI70|UP
zUH@LyC6W%Y65CA#2vlIRQlR@<8-!1Nh9T+IRYpYT-vtLxG~ZH=BkE}>xW=t|0)V8Z
z$45?HP(f~FWE&oXaJVu6oS0YS4$<*tM~`At
zv6&rI_3cNyBnbj|nr&?7yG_XwZFzP#wK-UHLq69z51vDTOnm{|H$acEdiUwCFz&*^
zs5R@Lhe6m(9$QVlhU=39wHqiy+9g6ju~On5=?VZ<<_GKgfr5>Xl`XKya5!8#YCL`A
z%5Sj}eGJzn-^WYo9OVKa3wzx*r+^@Meni8U_|j9=+3?jie>)cFrl`aWKP>=BU5zOr
z811#La$LPDhk#pPvl{|uLN|zY3hl~-21LbVERD)-esH3kLP^Un{-ag^I$&H%5$dEK
zqVZ{MjpYmE;4TRSY!i^LSrP$7cTMjFUj9nMY&ziA6Oy}aEQ%~>if1uh#Shd7mXzYD
zm@RO1W21TicO2YcFX&KKW#_u;QY9|dSX**>NP{O!b}!i
z9BCI&n%DibWaDBSI4J8g8dV2@9tK1Ap(f83vK;vzZ4Re@vfP0BkP|!4t1(qUW7n2K
z6DOdmt2@6M4x<}wN+JohmI}GYQ?u*fg)MIwa`~vB@(t68v70VTcysu77G{dGJ_J(N
z?10{%_(N2eiRmx|PO&e%%Mt2eZ+>O-_5Az)VSCH)v!M+fp#J0S~7MYU3KqZk8@S_$YZ>_kHmf<9vq
zm=`62`-+Mxj2~QQ_0m4*9h1Gfk$DfqLHwy~P|j0VqQ>X{c6`^zY?B=$kwlnoQE}C^
zy1eF&U4yaaag@}X3nYH|qy20ag`aep
z));UANie1wCKt`H7K~ljMZHLy@~0L~4btlwhi!I=Gri7ubuSf3;ujOnLMwY$nNHyH
zG^HBTj)6R&pMx(qW_%LyKj9wr;=O%2EqYCTbmM1P(A*@lFlWeK;wrh|TyMWKBn@g)
z6L~s``mghXz?I1KRr>nUX5p*TyR96h$FN_EW@)@1-`h={+^*D$3|{b_#rcxu9PKt5
znsqPFDT|%b7{7WR-=aVF^3#seaw$_`)CcMLkuN`;HN*
zPw1zrU2F*|m_-5&}fpXo02VH
zqF5|dmj@y>3uow2h`KR=@-=a*U{MmO1nYYo0i
z%vJ4+UVsn6%Q!=Gb1}gRfyETjWv-wTp77irBU^DxQ$v-w8Oe%Bssv(UB!jcmNPt(Y
zS6JvYMn$Ts3qDuby@@U(T;2-sm|d=(!}H0YU5knb;5eS4q2C|jfc<{&--UdA(iL*|
zG0pEPZg4+AVo^F>(^bEr-*Zaw6h+A4K09yZ*MWx>5j6~41kYQ(*8vxvGL(Rbmi}D%
zH-br1$7N{Ns}iTRrEC1XpC*C^?@jZwa^|+t`Y%Zzq8Wc)<9eKD1s!4>G)EzeIlL~i
zo=n~2m)*UMPB8|Yo8C%&h2*#DN;vWGk3FK2j)&*Z1@p+hy`SeI*vfd*1P-bDA@{CW
zZPwk)q
ztvyko`EKTVr#chRkRl|Lg4&WIu&gi;xu8L&=U{eG;)iX&dK0&+
zH9g-*FT`SRqh8&pg2Xe^hm&48isO`lzplXCr8G%Stn7!a_bTf$
zTP6#+z$yQ+n5xe7%hL|RI*yvlvqrgQA*=WMT^OV!bOx_9
zYO^bRK!R10Ls01R+s(bh)4*Fljy`eo&*F#8TGG9yJQgXkXv4m_JyIL{Cl}TR2*>Mg
z8Yg+-m}T}fAIehLMg6UT;3a=L1sL&lsu$;kZ|{1Ip84J7gCpJA2XmQliesaeKKx}!
zsf&s*(oe(4kCh%1j>}?LSY!BEfv<<=(B^?pH~f*u2!&}xaX2D9gF7Bj*wQISt;rhd
zOddw9ck08zV`H53u9bTqJ`NxtC&4?w9}`V(4|
z;bw(eu<*o1o6ql*BW^7bD;-!wxH>VPw@sb2T(2KEMhPWkMe}n3%*Y8Yh)CRFB)kyGXXvG2_qjPj{5PGg(^hR4V{dRSZYBJS->ACN4CoKf*ugo1IK#JBOV#O^RJz&X=1a
zJ31JB4=&nHS4)$iZ{yP=$?LH!U^r-Gdum8N!Zjji=);@+B1PEw6Pn$1d#Rf`9a(U>
zs%(XcRJ88Pz#I!-P5a6yWz8CtZrhU@Q>eeGboIklOi4C8Vx72=wpI-IT@fSp_^OwB
z#qXFzzuvr_HSn6wT7S-HYmP?S#34TZnaXf0DGIEVY*Fw6ZUx-Kfo(S8XLy5MMV4q8>yd=3BnQ*os@xIzMm?S-
zq7DKENTFAADDwOwOgr5m=juOSLzMb_=}a{`AY_
zVo@}?7PzA?V(T}nn6;ifsuOQrOj2V@qg_SZ)NC`^_)bszMS!G
zI~uFGsyZ929piv6ff`DMmxq6E*qRMAL2l
z9!53~-{uj56>tKKpX)El#f`3fjd0k;tWWN!{#TDEC)vFIPO~NVSvh;(uJerFJ~BYT
z#g-NRS=#6Ac6BS#HR5(sfgiToD$m@Sxbco5_*)6qG)D>kpTc(EEg!46zbOgE1`@{<$98!2XX
zqs6A&LpQ3r`S4Fl}iXlbI={9hZSRai{D<*a`ULoIZ
zzz$|7^yUqz7XLCN>^KKJtjO%!C_{|qd$a0EZz+GhiC}7LmHXuIfDdE5CyczO+AG}9
z{Y*{J)@}1znC_Q0OlI`FJ$GUdlOc7IQ*{T%r93HW0u+&$l(j-W(t#|D#xZepT&oW&
zU$$dh#lK;zsMpr{3P>(}RmC%$Rw^tuD8Ru;BFj(uX_^J`X~hi%07VcTOL$dCQ^QdCx%|Hy&7V3?Qk@=7$=iaa~tDSNt5XY1=W8j~Q}J@@PGJ7m2r?3DOS
zOS_AQ=6`K5^6QCTeatOwE3N^jJf5Rym(Du%CY~Qs17R^!{~6UBO#*e_N9ule-rW+b
zXmbiMnP;{z;e1hCdBnzJl4{4fv<&iG7Dk6gUV@IF^!TmshqpS=RDa7FIGNW
zPf)+|?M1-DDu&2f7pPy3j`qVzWx~esbeH|+ZhC%@#_K%<&UJ+a`v#=B6rr3%UB37s
zuVgg|f3B5i(Lgcu7CEym3SmyMrhkyTei(T`w2#V-_(GM~3M?!$sVZhaQ4!`g%B&?&
zkB-f(=+z_wjf-YTTgh9~+KN`fMNcDHh?AqS4>U!j8yTu5WN?P!U7}mHm&A>-88cCI
zC&v}_L{}oqu3z+5OKX^2b7_73l
zE-5Zk3TIHTwm17?2QRbn!}?XJ6JAzb8GWoj6-`80PSwRFsF0B(m;L0z|K^(f%7T?L*n0%TK+GpJ%LY#yZc%
zlpvxHAX`YE$g07_QPE>Rg?y0Bl4yPNYktkd3PBWV^t^+si6n1Ly0o&?>T%VRt({A4v*PL|3o5-NmgZ#)-Z
zF;;$uzU3cTHl@;D!Tc&`d6vi5QMgTReJ=On`p8cgsXs)qg`6CB=%s`6szb0psK)=|
z^Z>s05_e@bU%r5-puz+wPE5#?Z*0CvT0xRoPCL;{LR0KSl4Y3MG#~D0@AvfJ-{bM6
z>+5x!Mz3_|!aN>ycaJpR$CM7a^+v_)&cIc(eHozhTv?}EezX6{`=F#wIPUPQT7^js+Ei_$>X1B?fS?hxR@*q;l2eZOQCtFXxo7G~#(5?*(Lh?#T
zaru551f^#%1vJt0pQffx6H%Od{@kfaV3btxi}aLZu6Rh4Jaj
z+r=40Pmr0KvjQ?#SKHETSx)e#v&)iC4Jt2T`(LEc--+S}9fi*y&c`c>
z^m)YRw{mLeu;(Q6)TXnHf++0RDWhowNM<1OnYD(JfTCl5cp%>$4E$5nfoZL&3R
z&7F^xZOx+BzfAV0HAEm00@YZ%v9hgK^j5Qi6Xx2r7RU9^!Pfsp>%Ry!qk328I|{~k
zMQyh4e6Krx{@ywKIWW^g2cerkWLh#;#4)&YUQ@pDIC^zl0aC7zgx|h>Lfq%U*7O#Y
zEfO4Q`+nYpIJwp=%Ma@scl~`fY87JT&?Oe150Kloa)iF8FM#LX7nk_URn(jWZL8s+
zHjye9?&A&AS4b{qhYUgP$XFL7;&{TPI^yF5L
zutiy5l)q%iui1Q(CHEo6PH^|t;k*CBLeuwIL!T-IWdmrARKd+qw!)uuGq{`*hR+bH
zQFV^hjW{cZwsLu0faIC&XLzadjG=P03oJzt-P#?nYlK+}oq5?DpuCyp!2jlkiDJ$W
zMC!<`Q(okKYg{Nh)Zfg<1!yWKB~&V%pKP~pg#L9HSYHU*n!|h9VHK0MUdP3Lc)4eB
zRqP}Ub&N8LbU&`(OHO;sbvfWO8z6pAz{%+Bmzk=4vAo|&(3$U~-PV=w&!VesO{gGx
z+xAYjs4Q29YGAo7@-&V4kVZz+iu!svBn}2g1#%y3+a+%xqiLP4Y)PT$a+BtpifV&I
zXOfiYUAO%0h)T3mRs(ud`ZyNkPfN*edE-OL9f2f5$+(2P0H{~O4af_iB(C&)Qt0}=dsmm&R
zXV+)=b51lLc=LQI*YpAMp!`>?BZ>svD^I6hg$KC-WpleVrj8oT6k$y+!}&wYZ`}BG
zVzW=WE%I%ZSkvjnAiXDdU{0>Qw`7ms(r;ZrR8gr5p}d1zt2n?Cg`AzhG3KBCrMWZs
zko`_xnY{kd%c`}a?-d{C7+Iqw+xyCP^UoqNosV)&RUmesHJkJsYMMjX+tdF^l6~%S
zl(wLz2-s)6Y_m=X@KZ5m@E^W))Q+xxtW-e;}C_g
z{n96t@8A}w&E-e@qyByN>ECsmVfViwA^LIlYXn8oRl|oKKMpHT
z{_*XNbkq%)nr8gXje?I+TVfxSo*C8VQDVs$Dl@(7?Wfr?c
z!}Ta;ZtE)O6U$X}i()Ac_F}y$EM8q^Zv$#7qpkIci7Um~o9V+qrE*B$&r9p}e3l(Y
z6&rirsEuB#B9k5TN;UPX-MPaxJ9|Y?9kQFbQ|r2Kd0YkCF9!b8hl%nX*jr<@gIx|{
zmZCck2}@3))z3S8(R`Gd8NE_-evzLjBoC7N*_tSJMwd_K*e^h1$3x0-0R=uT>r>tM
z^fNnhrOlMzD3^cZxZ+Vaf8SGM^y@y-uVg1T8U8YVg}psJ<}6)NiR0fpeGbWqV)XFI
z+=9_i>3psmM%BS5kA^Jbx4SOa!i!f
z4^`P!B4&&5GHhpk{HiJ)xwToys+9CFoi8HX%;ATPx~oy+NnAbz!E&@xhCsDmW(vNLmP3pMkwo70WIHYN_te*n0d*eZQLoc`E?m2Pg+S*O;$2+&@zN8UkpC*4DmrNoE_s}AUy$x`9
zd{03Iodg@RPG#1Ah9H!k2u|ew_9W8Z-nw*7S+Vb1dfMn&-p${6^9(GI@-yv6hJDePU23pytR
z(IdwC-lyY=!Lgri{(Tck7r#w6ksh8Hx?VB%pJ8Hf4fVbK)Yh{$?u6yD_RJ`{xyccD9|nI-wbyXLgycEzU6hAtmxR
zHU3~#XMuEX1xMV})TKmj8bLaWzh%lv3x5pBKeMAB@BNCIe`V4c#Na=9qmDC?!+r%`
z!N-<7Iri3Pc0mFI<=C{{;QO9B%&2`Dv|~nAoZ~m^6o?#Dy4jRjQpV@5*<`
zoLmV}G}rT^jS@mCBP5}BDKWl0vwM&Tf|5q`6BPzy7{j
zugB!BzLZi9GDHB1DESckE$KO0@VXLZRua%jG;-z-_&WI`<9xD5X%c2xqp^6n;Dgih
z_`bMtD`p=TL{*);vT(k)5%1uGLjzt#V6@9TZr0_PGps5rc8m@N@aO#rL)08c2Ta^XXCBwgj!o0j
zOeWZ~EHWY2@;jAj9?5Twtxi8)VK7(LBWOCEE56Y{IyPFaY*?m@NrC6$sq!UtHs!yunY&!I={<7PPC8kQA_%w()U
zt!rF8ZFV5Sw{g}E8GUXaz8;BN>&p$CftBnPO?ne0vOVJ5h7{uexc|1g
zf9J~yO_alRImH`z5qQ@>`)Up#7MfNOtIdj0pf+}EX31o&R>=sTDsz|l&4?nm8u8Eh
zNjEiwv1)ZcOkB{gZTwDT0@_`?ux{9A<en@m|3>rtY&6
zlg^I^PM41<;(jSPgRPsudppODMe56i$Onr@R;KO5lWl=D5|$jBL6|mdBYcT}v|gXS
zCzcfVpSnrE<;-5f)7ZHZfiIS;uVZZQt-X193wJxV%;>Ok7H=U8*^n#o`lA+&NH@(K
zn5AM&E~LQjnDpac`_g=hN1+ThrqH(L?BH(ZkJC-JteQwhZ_V)Cov+D63$SWp~~*zNxo+B4psU8=f%wn9~E(|oFFu!gk!n%VZu
zkc^1N9Ov|M?SUpPdwz{qQEoH18NBC0Ds^Hn`v+%K9tNv8Rxv0>}Sn0b)UG@w`@P
zanMPpPQp_}oZjcBxq^v=bu2_Nn~!iE6zYRdOUgEOyEs;aOC=dsOnqS|MG|j}Y6fC^
zBf4L`LiD9Ceju#B=+#&r;5lrS6)$fJYw@%i^yg&!q;aM8QJ64v`7A&?W9r*j9`&UP
zd!+2Dy7JNomLc1RmRJ0p)}k$k^7^>2XPot@e&c?VtydgtCf3tR_gPgxiiF3eHZyHy
zUc1*lQ~JKPkI(AHU3+(9LC9`Il2{gXZ>~WIU}$gcdz=J25?@aLq+9%uMV6@AL&(+L
z6CC?f5Fp*U)yFYW-PXwvc{Br>Gy#D8A6-!eS$C(|AK2~HO!(wd$9-rg_(uHprf;y=
zy8yXiZMF%mnQ0BsqXyxcE|)O6
zk^U=BkE0n>qkXRN#tN}dbjk_Di5V-|4qF{i@^r7L{5F_iF=El(tX2&J(RVos=9*$u
zpK;Be`yf2Xwr2j>qq^#+_T&^H?%2k@ZX)UrK_YGz>0P}4_UJo@N^{*Hf*(>`_{rbx
zo}O=$ssh+w>8*`@YEG2F&Z-tC?
zTMg`z&&WvPNYbCCn1{eKN%xpGF73
zjiR*&Vsbv$_lVAxe4K|?USgPlKge?taAh5YdNuUf1eue79q67p8J*GnZ4}rN)S0Dgr>HKo1N8zR
zyrJVgqRx*T|EZPnB7><1gYfLyuSxz|c&Ht=b4MGYtKzE*e>Q!0O00iu=+VP5UeW>=
zFfA@S49W@<_|3WdwKpezxpJYjMfIbTHzun};)~N;!Q@RTr@%FxJr4%LTZsNw;G9gLKEE*LY<5t38
zHw1OG`VA&aDS!M+-#hl9`*VY0S&Vl5xHnpAd$U*X)+Ei}z>UsA<>GMfPj7tuja}`E
z`V#J`ss)^0oc%zj#q1`pLXAWax(g=l`U~YWBp3G}Zq>kiVgfsttH8PSW_Mz36Blr5
z2b~cV0S@&G?41PY8WaW(YjtpZEXmKYJT0rav|^ZpYf#J;pY;9-ZRaVBqE~j$2SM;uPaL!_R{J(ot_QB(0jaVYLmf;n7$m1TNrH(9J
zZRV$q#Uj}H(|y-;-4%doCMRQZ<*&>gv%mCqAm+vak
zXcQc+ps0jvO+qse@_^}Tw$Meu^S@JJu>9^K=^7@vuKzaXA-Y@HIm9#Wue3q^V9jzQ
zP)Xbb(w*zw1q53XO;)5}-64ju#<^wHq6|kXnD#qxsS3cgf_-^yw?JGRiQh-chv-na
zE);v-2`T6Oirg|r4LcK@u6GIS-hQT0p)ok_U-9HZ8$kN#&79rcH>uR=N3JD%+tE@V
zQqEHz`8zm4f=^f~1B56vUc3D)X&!AEv08Y9Z?BG~K{kI|86v^2i8
z0bIv2LDi2;z>~Z@q?!}!q?lQVA3Q>{Qh=YYb!;V|;jJrxl#+S>#nX{fN^5M_
zBw*^iQowQ{Gq$mS>?B64aPpw>RF+1L~j=3+E~wo<97V~YDjjj@9lD;y~2E@
z_vIn2%FGRAry+@bY+Cfc|9PMpS=zp6TQwcaS1#hhOYF^Eqen@#B6
zR>iYQlkSo=T4+Y({I44lpEW^UyDfj358O*FKxR^GjEjSt9u|*y?D%yAYH5o#xz{rC
zj5Td#f7MUdYD#d77IfvD#9NbtnQG@_p3WFOjk?4;p0Zil@T4JMpYRp+On>jo{(lZ~
ziC)u>6zV3VOx@o0Uy)>Gd?EgQo%~|2eI38~jaedJ;~IAs$@NW9c3Ki&%iOYLo=2;+
zl;K=znW+zM@@quDvC!{$#w6JSt6uG~S-}3e|4qCGf?iB$2;75YtdPbr9myQb85GHmqPO
zw7Xt42zt->=0{~D*AF#U^Wol!yXrD0RyouRp+HAw5&eW^;Of$Ud=5wTg*g!>aHc46
zX;JJ-%iSaQqCVT0D2hY6n))PXt2Nnlh7Gedn?JD>$rv@cc8%j8>iA&%U%|r(G?k=?
znc@!pC~3K;#yi~@)GQ#5srrY8CYMS7es^ro37eeH(UZALOAbX`Tio5T$i6q5Umy(p
zkEiAlvZ;!`pas`h$w$vW7fT->UkANRhEo3Vs#ELwq-PDWtxLs5jLyoW8_`Ol;8<;4
zK}k&iml#cL6VLp1&lgaLx04!@rBruV{u}4v@ic-dd)KFncLc9*sG3w#~{-s
zKEnxgJBvKd#TeTz7>`|Gw$)7zRkY}fK|}s?|>K)-Vmpday{H>
zsk`(1>yBvX*3U>YqpJ4FI2quQXA#6xg5adT0+%!O<m!cBL^=-QcPTf4i+$_PR&|FDoZh>6!
zurQ~O>Cx*%t;1tNu*2IMl8kh6&6P9kAZgbgqfJb_>YCiPZaIICFW#eQG#veVGXlNJ
zLJc;21@>4^Mo}if9FVulqS<2iS(8aj_O%ZX4==0>xeT~Rj=jTj|&Wa^>S5tvs_i{7Y~j6ZUGH+*
z9LmR0Mcq~maZWRS7uoKWW`;e^py1=W_&PSdylz{%_JuJsi*P8Xn$O37>hrF*5j{(5
z8JWK)X4Z6NWMpeJECBXGsY}r2?Q+z$mDtWlTPM1gQS_yl?lOPH-E-q?dPC*=EB_q{
z>subNXk~3);L@~F*G8lwA&ZBr-+oSi9mHrh`5(jd06UQ
zf?|SoFGg=PV(l11^8Wv%x+y8otsJi)n->)@}=GurV0FsL7~0nM4dq${I>xlc)v
zJqm}v&ILAxE6g0?fh)C+nx~f`Y#wSZ$kFNN@PsbYX50VZfKBr8b2&aJ_$RApo>UXb
za!ikCOz<84w=mB={~>=#uTA2>05)zeuiB*GEaE$a1O0VwYh&KnH4t;b#@ph8{XB8K
z#|^(UuQq%sIL0X3qyi1Wl@%nNE}%OAFt4gLz5t9V>{F_Wn7C|$P)5hX9u(T^7PF&{
zS$fhZN1;6P?-(U23&o*sV#^^V>G3s{k`thk-v(O7$^#o^s_fjc2cK|4^
z-hVvd#^3JhA#ZPR-|)**ZBo2Ol(Q9%T{
zaJ}CS$-=yfe;zS70>=@!n3>Jf`x{)c>ULQF)O66jazKanb5pJjHXcD7o|;p&MLBbN
zZ=5{{jx+s?mrR#*1i!Ou(ugcjhpk&UZh_3S+1lT9@$vezn5TE#6hsq{{jYnk1S{Xy
z7H>S&6^`(l7>@MJ*4?AL|7@y6a+k^3(0T)Uzuu|fjr#9FQr3FSyl7^HYpsNn3`7-0rICJGr$VCEFP}d&ljB;@K#oAFR1)=pJLX^VNd%X8XZ|{Tp1<
zrSmJ((WazM?umS?r+Ze0ce?1V=z0|a>ndX(L70-t7_d|ih%chNHLX$LlE}a2B$Zdks_W`Yi2<`YYMu^Mv_ewo=)%p%|(O()L>KKeQVaWwfp`+__j6XsjDsH_mIp
zN-ui}`FeST5o}o*+A6Nc9F4drkw`#NY~B;c{sqaJ_eZLvni>`F&MQ#M8D0N6P@)Ck
z{J$uU6xZ^-h&xnlCZZ3QdmbLK-jrx^{Q84ZXJm%OK@K`TT7E0ySEgUZM-9wLNr`<;
z{)Ps2<*&-q9OR>%^|(#ZSScHn`uTzt{?|XNB=GL)oM#<8KR2z>
zr0P<(h4M)mYbY-aybfvr$EIJ``L>Pbsi$<(>36mbEW;U<(Mn*G+8C3+3+2UjuQZD+
z691UYEIcE>Th5t3w*yB0I%T>D0jC|?ZTBr`FKA5vh{nada^GLobc{~CsRJDU1k@w-sO8L;NNYaKZ;g6<*GAouEN4mcj_-0e{P0(_C$+D#*
zk}~lJlyE+CI`EwTcw6`~kPx~IAMPr>2m`p1+tE?(Y*8fIQ@X+X$whl5F`WtoSnP><
zi(?cZO-3;`LUI$&o~GdNo3a21ex1p3Jqc`547rANFjz*;dvxIUZ{E&G$~2%Ex1O${
zd+n(e8vS^t@YJ3=`)5+Q8yvWbo3GnB@8f?q9!p#Gg(!zaF7%D5vje^?{y{Wi8j(>B
zJ_UWpjpWI@sP#Zc0nyUN7b62N6%V}ogFn|Bdx`~K_0)~e4<~VYeD}fQjeqaVtozd`
zJ4TH&HETD1Botwf5S-s-^86o6KaJoHyixV)o5!9S^CZLZ?dDYm!LdsC=fcEHbTz)~
zd(^Cv80Kw{dvkhQ
zs0ezQEg*R$#(MutVNc?mo*K?`#ELF=Taa-1nlV@9!!;%p)IX|KKZ;kYoaHIGrc(C0
z*@08u8TyaBkGsmN2n2$iL-4prfA-@mwBfs4L?@!IQ?PV(5LQ`C2#e)}Y{
zdxgjSefxT*vE{c14aCfnO^rPooVFFJfxlO~Oej`%tGUkxBnPaJt04!u`5-7yzx|@l
z7EbkoOsb1$!H9AcwqW1G7;?8goK?**pIkE@#I0t~zHd^on#
zr0t=uP_X5ivkF)^yUG(XDE!SHErvDbc;AH*a)}%eME8G^*r^fDS@v{St{eYsrZpdz
zpphS@{uOK(ReFtvjwT~5+k@y0XXKO^Z_9Wze~DlRxH@#%(1MPdD~DxT5O%3^$o+wt
z*GJIw00ig!H`t>}#?6Xh*jD(+-BLmE`s0IVW1gL!a9e0&`EbP8d0?~C)3V4NY(V1M
zg6@BxLORK%HJ6I!02I5n^?MZyd4(I_r$5OSsWFShu38gCP92u1Fm>@73e6F06j@M2
z>2^H5oUBnH1`Z2(8^PD}ELKzzqpy56Bj-ye+ewI45%0YERCY&50?;NEH}Hj5>tb^v
z&FTpEgy*r{wX=tX;RdF?fPaX6WLWV2TR091M51M)wR{rHMi9C!Y!IROSS#QUWN*UHdb
zwpHmZvqfO1fAvMezj_i&ndatCjq_qf|CQIh4~Gw`{!WtN(Pxcci|x^}Ee_6!P_aKjoPL5lT
z^?c_@@%5-XeLhLZNs;xBY}6j94T}Ms80yZ2XxuY$D{rrh!fBE(Tdg$y1H~LnUKdCX
zkm9AcYA}l40Y4Aet?<6cbNgk!mhQW~*DGYXGc*R~&!x3uYcu?O37OWU+0NpP1>^-o
zz!N!UQZ+Dqx%)S}YANI-iKYssyh|x_UZkvoqfVSJ&Y~~Z>d}<3
zYB!pDOg`~l)(ltve074$NLu9bx2zW1S?~NJmFZ}RU571GRKN5x5#FzhskH`+V?K3v
zYxG1@$}u%wN|6Qx^x&aDUYFY8(`svyyk~xjWXprqP#dxiO8Le6+V9!9>wb#ql5Db>
zyHZ$ry+)=4XZ>tI?b-s#oW&2Ns{vK5f8G(dZng}AJM{ZiyN>COc**4aQ_>j!a|Q26
zIH98>vmyHz<`F92!t9iQIt#T%Nw8f5mZN{B7VXMy4Q(M7=-A&u$`9pII=9}3~7*rRMHyyfZIb`TS-x`8rEHxV@zE(nrUC!N)+XaEP`=tugkCzl~0N@{dS|0x5y}EEf>ggrXlQ
zVb@6ILM5yb*QPm*@P-Fd`T^8}V4mEjdx6(DL!vgzA;LJ{T~d;7nKo5a#ObFsR0A)L
zL0kKH^yyL#ezdT&J03_X3Wnx{^RecWn*0shvz!<1-+Llw6w|u%Vq*<_J*cz|v0OYZ
z`&HdOhN%K=?-yhgY*o<%_f09P0G5ScxYbi*6?M}CY_6cXY*IC?-7G8hQ0BH%Qgh||
z&B_@35Ymjwm$@#?8+RmrGN6R@Jh=Ug=#@vWM;IP&k^bOVoK0$0NHjy*iQng_;g
zhBwYZzNAs@1V@i$WG512gP6W@IF?L02KvZrykc9ki&GA-Q5K7K8DL2J0+Z}r7Fd8+
z#MCD*jFqY@Iu}Fn!6|3L4?!VQ(SBNqhk9AU>MkoYsG{wW5&q+YmW8m9OX>?JIH&E(
zTgd3^t1aR~!NLxHZ1&8qb+PtP9pz1E|FmF<9JLYDMX`RSDt)}!mwG?@@B}ojoo^(t
z`(@OpOg4|PqS5#9^yYv(WIA!Pw{@F_Q;&@%SN0GlX=EJyZmtZq901n6xgl4ltPd^Z
zihi=wLwHvV-&hO%5pe@-3dwq{#T@=?SgztCp9AA~jrq=I4Xju=%ZelC`nxZ;^Umn&
zb4hRNyV9p>sZYRY=W0R|>cxM|8(!5%5^K&CmM0ilApzpAoCSKt!HP)pFlMVh_47m%
zRj{NYyc|Rat@$r*nphFyT2ys$YJ2x+15w2;nj(Jv{II`wBuI|iH|AT-7$$%1Qbr6w
zLG4PH3+q%utCc5Z>)t5CmI=HYD<+7B>3_tfKBG+Jdtt+pQu^&?x)ZECFR@Z)!VV
z0ojw@qa3f%eqKNjBAfX1GNd;wIE=-C*;+bhc@D2yW|5Z*6v|!V>6TKnh#FXyWGOd>VvZj_Ww;voEtcN1xNP<>
zKCq(kh4rPqlJ!&>^v^zK=fO2gu?S|eoeQr%`nz#^*JU`Ii_zu%4W0Q^+nG6K?ynUA
zi{M+DB};PnFCa{gTgK}#Vn94(#gQ2J`_U}veOKVtn(5ro!=**8jkIMz{c(s%s^7{s
z9t?rltO$}FP;AE+hwg5JM7eW6WEGv3^1OOqrV(orDN4FCiV<8miH&%HhJL?u4HWgP
zuP{jFjy!nub8)P>Dbo)NMxUVN8?FHYZbx6Qm?55U!<^?Cj&&Hk&D%J}3P~l;A!clS
z87e;SZZj>)aWqeN{c7Hx9MqR?@iKU72}Ud_wVMg2%sgV=O%w1RqiR9zEJe%*^#;c09ex}fs`iXcq^hm9_Esxa?AW6=
zZS4_agcxZ{L=Y(=AtZVHUp%kxm-l^tuJb&u<9i%arc9z<;*BrGism}z9YYyQ9mNQJ
zA0d6THzZZaOe(k3D<6x)spS;5a-W_S>{^NkDPcXJ05hu^w$t$+AG92b)
zLgJV2)Z7ZB*7GVvWf5Y~xfrCI6Cw2ye|E1-t_MzN**SNGJ<2O^0_zZXu}U3JIn
z@CZ-Bq&2hsUoZ>*z=&;63-%B5cl$0N%j9pr5bKrrF4zo@^tq@RL)06Q^WgML9-quF
zpj5y&hPJ$ezbjfqRmDFoi|!wMud#PCc96ph5ME1@*}S;(RAMFmk#08TsBXs|v(NXG
zzPTmug{}M0hK?HaL77p9&s*_I0@m0_Rh6|+4yF~s<0)689I*QsG9fHxt?ytV&t$1>`K3!l8y*!YQ=#>qNPx|R{|3b^PvO7=4V!LjDH|e;dhRA{;
z#x56IY{==R?7p`mVFov`zm;@zxze4JzqGZD=7|e+V6zW&!Aqfv?T)*rYf3}#cc;gF
zPv-`IU{N|$2pehG%~bLg7k>bluk$|}HIMwLfh>v1^<%2xRJzS-C8K~vGRDx^8TkY#
z0jzji>g0Qr*THYwz$ZExJQDDRL&UcAEKs2^O%q#SWW8GB(Ccr=#06ft#$0VM?Ww9M
z+yKvuv7p?c*+1`WeaLfNe)%qSO1?8+H={;Qnd<>>`*jGtnKf`+~7RRc9
zsf>*pG1#!-Kg
z@%^rV?wYyHnI*(e|
zNrKGpeS+62gQF!{A0^M~9g3WzEW!^+b)J&%#I$f7S*=0@?{2|kD)$sDl179|QZfX%
zpULrKToX`yrYiQMn1(k;@i5gx4^>j&y*Fd{Cp_UMxr6y
z!*9bq(1#|_C;Z^3v1T
z_DZ4moD>ae1ZeP6Wh6wr=g~mr2uKYs@
zRw;C%aB^sQr(OxK(p>|tyLUiT>4wvf@p8>Ase{UM;_9zi-Plil-M8jXY?FV#FNXXn
zUl2V3#P=^mj$X|U&F^88MEh4v)3<)-<%GYoZ`*lwIDURcOb^83T+%pGd-HQD%@e7-9p
z_o+)^Az*y{=L;MEgPe7Qcz%OHQPZ(>SJ`bhQsG+p#PSi*_U>GA=%)bZ)U_B0Hq@v@
zVB`
zS!j`<8{G3VEODFTu$Lxc)sGTZc9P$TDs0{YzX!+cErsy!&MnlUs=?J#tTT0)>~4j06GawZS-
zh~wwCyJFU}MfzIaUGYX4%R-!WiD5d9#vB{Q^3q4wWQEqlEL`X!w_LnCdvEz$I<@o!
zKn!|SLG*(azITn054;|TEUnx01K3FzGz@p|#$3QBi{2XE!^cYB9Qz!{yFi7L<@j+`
z%BwM{Y(sL-tvP}&n*5CMi`TC(Xcm2QZ*}W@7)5iIsg*i?+1+<_adgz9&O0bFXN-jq
zV6Uon5Kx<3e9}9|*AbjfP^epIyK{Q_?9ErdX8@#hZQ7)8^C$=Tr@b&f3e*Zn?=wVf5y-IAuQwd
zM*doLv+mB&0@;|rvqAIQhYsV7NeNgU7lrQ+E{QD9
zb2D7F8>h0=Bt&cN%vn&<$%4<&KEkmdj7*VjMZc^l#|BG+2ZDfeHQ8?XF{1C%pnpGh
z^Jti~IwHrG$^W%Odnfpq)rNG_3zZ$@N_+{pZR^S^ToYFk@ve3
zZ@b0vx6+5u1&;ttz|*pxW7|2-`M1r4`OnuR_3{Fy?B!HAp8X{@-Iy*KXWPyR@;kHN2Z3#-RR--u?6O-?l^bt;TK}9d@(!h?ZSQp2WL5B
z|IyuGr}hc(GBM;iOaUym(Xc?Jm(MnN6HhFJfZIKN
zXK90%McYNRRJ|7Tyb`@yC2Eg%w3brf>bMR9hl25Y>Bx~|A2O0jaNkR%^viG?1Fu|~
z;sV%KaJzV*t5o2f;vllaL}V(5QC@7{^Tas^A@Ht)=cP*sV)PakhHF8VFQl~AUx
zURv8D2XOQB`)|5R((`wcOdZ<3bNF#7$8D}dc<#>jFQ7`EuZOxVpSX|qv)i`0%#{P2
zS!6b3ZG-X9ptFhQxVV)|pM)1qm9LVbCwK-mwE>%+U3{<|zSRPWWsg4}#9!_iXU%IE
zLo{5jgXLE&f{h8C#XBQAVT`kFSTUp0Mq@^nDv&6(tDQGu!^qkZfbdtQ#D?1W+0@K#
zDd*kM!yN=QxUA$V>D_$3<(y1l`Rg#^f_(>*Np8z?*BWuIvu{u@ZFRz!B9))qHdmmx
ztoT>-UNj#-5ju<
zxd$g6+7Y<5W0QX
zEiFb=H~QXw|6Hu#Y?Ow66cbYC3VnWnP&Hk4WAD-ZlN7vd05h1FIpogfM5xKwL>K=K
zlc3s`D&;iktfjpW2G8eB$qXa9)|^LW%)CM?so8kfcneV@F1r<_==XK`!{`D7IYfgm
zbEa?nYxmpV$N?`{2W&clwlAIIH|Fc;Gs9?$tPuN0p6_vPKeEAJsSHkMz35qQ1JV;kiXX;|
zV;s}Gstn>6NP+7WZT%`peH%CQn8jmyIaQsAY^Ff6DNbzb?Ln@=jATf0DExq@N%I=x
z)=P26YW<8t5&2wA(=<69%G!NL8!``b8|?gt_2x{7F3VaEdnD&L7{4-e@W=pv6kq1h
zB#C*wz{GUx^e?`C`&7&|Q=fH=dF4e-6pbo36+M_C5J!km^X##HY=lqDw4tfFVoeOg
zX89ZHFVJFt<@UCf6QfTr-R4t`UDL(cf5pAji$e@g+a(1*F^ej}9$MwKy6&nInPetS
zpQs6o?UJ=4YNHP(HZsQ@=|>fG+RPkS2{YLI@DG1sA4v?zs5=Yv%%&)pNq*KIzuRL`
zmtnAz(1*fH#lvR~WYPT?{`v#`6((Nf6TCwvpETSA6?u
zT(N{L(a)3o2*_*8gbmlxsCa#*cG??i8&`(b#=Iw-_Q@IHD|oZ?Vw+*ND{N1>nO&8W
zXUt@Pb`kB;A-W2xmUVXax~}g=AojZT43)fE022&tI714O{q7tVVwZyr&-n;v86#BS3ex|YC1e1ySFr&+blXE
zor9JjOP}*Xw2bw&sVdsG{-H__RKy%}9k0a$U)q1jlI4x`V;5VSkgRBZpD7Zb=f^(Y
zGOhmen;eqe^NLL_FH|M!D_@Z>u?#k`rf;q_zpGtG*La9tMK)wE7*ymq04A2MEWxm>
zqvqKLyf4;Dr*puXNie>=erCSVoou`7KWt9_G!)U_v!Q3Tr1i~s#?2hI4&{P^jHlf-
z=k2>zBS$^QBdTS9ds0xGJ#$VDaQ7D)v+z-hb=8_RAl5`?=Bnw=puOv+Qz!Yve!^AN
zY}M^?O>nOdXTHnOqsiK3cAr~*kAwHFe|30iEm}A(gpOC%84J!&%(W`IR=$R=F1Y(E
zgJC2
z34sQt_2tJFj$-Ueb$R8$gQoYy@_^c=vY2I!Ddf
zQUkYH$LparM$AUFhw2J%1L=lBD>j!i`1pG$Rge>Xz@%#Pij|1mh6MIc-#~dJlk(L>
zl3pZR>_0@5UTIt3=WYInx$6#!K3up_#kKdj7^!eA7!I@xpbpYFA
zExc?l#OPUtoD28sfpvE+DAA6XCf=Mt`pA<86h9x9YUgd>Y16EsIn1I*Sbd&A#A8r=
zIJU|!f>G7ldFW{-aepN5%=EOAL~eFdkr(or!tz5Eo|By!3fg8OMc1Hyfq~)e3eLZe
zB}At>rm1G+lUV}mf}hwtJ3F!^F5iZSIrODjc?3S#dLQq(bCEehEqj+bnTmbvDCh>j4*M;^^qfu^9n1Yr%eeXR(o^WJP1Yk&FX(AJ
zUG?)#-<93V4;W`v=MII}B5a{M_m-{b%RXq10x8G=CNdn7Ld&eTffe&^7TQK?Olkd`iC^ofa7vn@qNRK*Q@0feWm;tiQ?Q$og>pq3ks}V?>lY^%*wM
znXG5J`4Gc#Mfc##(X7+LPKDdeJp0&OW&Kt}W7PH*sWw4()Can<7K!=6s1m2cthbGa
zf5JN!nWS?h_*Nz|CG0`S*~KTk2ekGrm(e4PJMH2JQVLZ8k*t7vP3-dQen-u0tLX5j
z7%lF{wYq5-^mVpVXXexzLF>ngo3}$+9C>7zu4C4f&$5jwoL$=PH!^nA@_)dwemTK+
zW4>_rrL4aC)Jqe)@`k??cNWT325UtX0WH2p&21{K@qi4N4n<$o^!x~q{&n`F)r$mF
z9kr`t^;&lr4}Jefs9n_+w3YEYUy(m-V7ICm$Q~WKMtLV|ge)A+J+|)@shTuOJoyJ6
zI^MjFpnVQsk`S1LymDkJ@=h3{sD70w*)07~h)8i9%V}wUukl!dfU?v;{#YT|I(FSIbz-?%
z-}B&?f~~-+xBT>fcLsl~9yeApiDg|WX`OhmS+YoVB)op~&4D;`aU6aR*s-%Xmr<(N
zhM4d_wfQ@p{-}AJy8`rtd0*l3L5i6yRIOh6&^Q~p
zC1ODNP7s^$xwv-OD(g#7pJY2o$3S98cNKis4f-ptdv^IE2VwbVBbK)|L?ylLa2F?I
zd!mIbl&67@h&GFYp7AZB%JI``WAbEom?C4X5!L9jo>okOjh2Ok1#mAq)W9GE*MZI
zSn9>6PuXvL?WAcGu)@Je^m}1Pku0)#r)bu;EVESG659ByDu{HVbTT1XHT7q)Oll5s
zF-`JZ<~O>!xR-biYK<=6?)*G(G3!kjwk5hYtm>omTzv9~Iwa%st7}DhcRXSIcr`jN
z8PR7?*O&8z(1R*QVtV6;i=#P66vXXGx@fuM({}lJc=f0Y56{b&Nb64q5}Crf)br>x
zn1%-4i-|7_J~$>udbqDIC!l?2ZiW0@Rf46_2gR@UHQ3W5^^3d#pOo{Ay39q*zyQgY
z-LyLH(Ycct=3w-gu``fN*&x
zYq>YAFLebR|eUFLbinea^YZT^^IP*9XU6_*lQyJ!>`a7vC*D
z_O7!t4;Pyfd+!e)Ts(z_AouZBB()Z#IYhd4E88K#&N9<|pMns=+UX<9{T}<=@)X6cL#0qp?~sNAOb>1M#LTdIr8tqLg_X|OgVTt8tjFq?m68D@
z=IA8Wxn6vu@e%9wDqudqDZpql$vfSy$J6Dx;=8_(tD%XA(?;^4{uEu0RSTT!O+9wF
z!HJ&mSVK$*_?mw^k!|5j@BzX_ZvkCAmVI<|Tq2P*^ri-A+hyexL|M4gCAYAVI3{%Z
zYddG!H~AaAZov+l{-af<2eCcumMUJqab11lF;-4*2Utd-lp=bO485tc_lsU}L!Kvn
zw2_K%CArfmjNMYjNBMuo%_eSh65}cbU}?>3aQpID-KQIdfnJYG6#cphmE5mJTOLFB
zg2S`*sAW1LrMI6cE~+<(_6TvR?~mTpzGBAPxPQOAJzu>-y*MxWNkzuL4Qo_h%71F*BLD#}xKjRnSC^{3JFMV9+=yo+-KEWA}W*UTMirE
zvOZVmgP!slJ#RMhim|e2e6*fb)OHQQG=?5oegn>m_KTYNXz@*(!qAv-RPw!WCCtlf
zEUaptWO(Q){t5c)vwWRQeW-QtxXzX7&9Zi(bn=I@zVj^Z{P=V3_tBH;0lgVwtB`-b
z=`ckpP@c9kT6Ea_KWbpgCzt{LcS1fma8Ux)Hx5)Xli+`ot)0*Hb?LM2sZOMBRl(RH)6V
zn)=(GH%!WLpP=L@KlpxrmKQ566&|~JRqrTdFWNA8`w^Qo|LbLXFFZtjBdEgLmgaOY
z26O~mD2sch3p2jVz;F`a8{T~_wb`hVXhH@GOL$OHrjjiPFR!-Q!FMK+Qdmu&HTIZY
z)lOjG6#kq(d4CQUo5#6OES8UJ(R9G6fU||7T8>6odxQLljsw4HDMk)^CZ88HeA)r)
z+ZfiX5Bb0|ZEi}_VdeloFBh8u+;VDeXN)^Sb|=ftoqI+;LVaDnTgoU`_lZ}~bSY-K
zM(=Y)f5FsLq;&gUdu!o=t&>>dw><%Rb5N7?Es%blLrOH=smO22vVz(vtK}Qadt8%6
zF;=1%)YQvA=mn~TKHL<%DnqG?=>?TRX??*eB#Bn#zcTwGyS~d~2WiVvk29IO#OK?_
z;WB+?2B`&7R}KXj-Np51yT-h%dxcIv;`|Zy1j>vwC6{tLDlhu%_*Y8gSF64YgN5F!
z^^hcnq$voQsghiitul%mF7KUzx$%En%qP)_nq3}{XNA#=Db%|9WNqu=d*WBWHxx9}
zfE@XxyyxJ#;oaBiyT8*RbH#VN`+5($0Do-yG%8v)!LiMnzsS>&P4}%6@C%G7@colv
ztE=K1K{-U$GLNulRsZh=Fl+V6umJ1TP={(c<&BreBf$wB0NU%_smPf+nKhpyHz_Js
zkOjF@lgpDGRb{qf&ShC`u8te(VGHFVgzk}UjFG2s!h@Cj&13;s0b%K{zUU#;F!v>Q
zO%I3p0?$W5H`HXyGrxc~HKJ?x=!<~4LA;>+d%VAze3=MX@7ZUYg2kIBs8sfjh5PNo
zrV+l_H3{TEW#~t?hx)-u$^ACQ{<gF3aZIfpsBx|mPo#S+aKkjo8pD8=Bf~P8ve!}ToBX&kwKXs=u>vyxID34^uIUEp1
z1-DB$$u+GB_dARCpU1oe!GY7Zzhh^=8?COy%c1C57#=Z8`}R4S!|$>{ta
z=EF7S;Z)tCT(C5P_yOErC+aE43(f9n8MQ?kJH|j5!jJk+AJxd_&>P2xPjCN}UN!xfQ~qDiY&rNm?9EbT+u*9#gHDzTdy9X(
z*S5*l3ix=yQg&bag<5Bxc%YJ=^3m|M?_
zvY8z)HDe!kQlZLyonZstvhY3qt{3S!>Q)~eR^fJ{Z(r!u-J|Fa
z{8eYuiyP3K>Ef{-H3Ci+lq`CyNOZ^!*$Dvs(K7o_#fZSRwj{M`ndI
zE>|=gE^I8!%fhw
zZP#IFgvhgAVy`)+m|xy
zU6u3-rF*vL4$gZ#I3$U;Zzt^)FUSF-50^23sm>#9dI`Cv2+d`2;Y>w2LznQ09y7B3
z)sem%q}jDfnkpWDTD1pPJ7jacW~b}ueD>URWXbY@TzCVJ0lE$#x}Wh}YH=vbVjagQ
zJD+04v}W?MNkG%WOVzN?&Zy_Tdb*rKW#mSJz76B9RpRX0PY^1!n~nKF9ke45t%It8
z2-Cgg=>-oYBeKHvU3%+Jni~&;{GLSrS!lG3c(fo9S}MKgOf&M%Yr&Wh6AAw`G{`oR
zK+F&hk$Hf#=8Ny;9|08}(rvE!d7#*rxtb79T_or6w?Gzt(v&!;$7|d*9UGuT3N_};
zewzA94+oQEQkC$S4IltLEzQoJPP?7QuiOJx#C0>06llCEjwytV*Q$s&$2EE5z=0~M
z^2tCHf;jS|b0U()$CD|msLUNOK_?dd0N=y>K_>pvO$7!$pKI?{YtCT@;K3teJn_5H
z@Ph@u_l1ez;*0@#PFuzuDK3v$mtmpdxI44=^{`Rj$cMVUmEU)7*=El2!hzPdivAvr
zi9IBL<0^&-aoIA3p6r;`ewRV)6S#Wk8&g)@Cp@1>Z$c@xg2)4f^HBPHU3NkOgH7Vt
zNBi#~i3`Uz#4O-LAFol&vwp)vJf?ZPVza#R&2mdtb8!07kNkPFUeUR1T_~`@CG0ip
zmupt~{`sE@J4KX{+PWy6#G_KB@m?`{pOV%~%FmtN)19BNn^HHgeA`v#-wf&;bb^0W
zLaOI5^J>D3n#Zo9{eNDr9JrNLGy$|&?i0M}^TbA_Qaz`1JW{YTfxjMC3?==?f6_0u2t)3)!isgh~)(Zt3QJe=*9QAVcV!jfbz-A#D+c&s91%?scdf&2K%
z%mUG$WBZ>orNTaWX%o`|H1$0CRvaBKcasj1LrogKLk#~Cm<5NdNj@J$J`$BKjC^2U
zXror}k!*_fQC0gz5rCkeNEW922%F17z?&SAhDQzpdG5UIR@}!21P9QsFj}vN4%g(Ky=w>lMy-xTMYPo@%M2+tN33qQmme{_M%rCkE2+-pq)1(*hU
z>@L%__79AO3Ky70J_l=Hov-{6;4*(b`R~*4a>z+p%Z=wAV0;&ydpdwx^&en3w1zs5
zyj0C;K{Em+A0yiWox@2j#z;d~P
zo+*&qo77ps{bM~>>~Z89Lnhq;5#Ho!u~o%X`>rtcw-+txE&Sh@+Xj^C+knsiu4U1o4pUi9)%KC;RAg
z)=>f;p{R~0a|O0?3yYs{UWrcd8CjqI9^K(TFwo8xY`YOfVeDm7D@~vO0W>V^#o1I@n#BDet`=)S_llvJYSc1dyJesLyJ%2noZdIMC
z0hB%*UJPYQ0g1bne!*S2HS0dh&y+%W(T+K$j+^E-^)4$d0#jn=z}l{?wBj?$a46xV
zq*vwLDW+5PI{h+U$%z7aKyxpUU+haaJ#2R={8?ti`=IokF2s-yQAxkYJ$KjB?DG%R
z{fihcz7E!o1W{XCQO&20_nz9A@rFk6t}Hh{Ql|NVpMo~j9-OtJ_K+>G#&(dJkDI$_
z_SqT)565`I!tfHZAhEvVQ$OUe)qhi`2HhZ5uqzZKbm}E@T9@&5oCIyzWA#E?E(4+t
zF;bLS)-;?cH}@T7oy2ogBJh8rh@eTiEbw)AXm^VcgFMJHzyqA
zYCZ)ev?em{%pt4G{RLdD0VVbX*H)VSKAtALw>y{L(!6uD<_d;D-wIc}Y*{uOlUEcW
z6!ipbj8$+F-mZ46r7vO1oCNaTsr-Z)AJmu!7hgou+IgRE`7ow~owrQDM4du$CI14>E@
zc*=s-{Sjb{R#T7V@}Y;aa=#?j@V%j}6=eV)kPSnKL$wV$72cUx~1
z2m!fsSFEk<*OyJjt&ESn(i>I5lO#T~Cf9G_(H*B&j%4rulquj`YIDU1Yemw+S=E9z
zZt}@DVBKI^`rz|Iw1n7fsn}ivNLi$NdBnS@5j@)AI&&YYz+#iVGFmS_>d9)WJ>GPHL7==uP&ahYJo=nDC?)FV&8=wr=F;+e#e($
zs^T()c{4KK)r;_F42+o=VIP|QKUMIcK&Ak*?6qI+!}+(DlgHqkQJMsbcavrTxE7oS
zA#`L!X`>kLjS5_*19(PDaxMrIqUQ02R@bMwx-3R`j~hxXogXg$tJZp%=JSXeK0)fk
zoqs8nKAl!pTCgY=4~a@5V@B4CaOc`>@=Yx4kdY%_O35vtQHfuoP-}<$j8ynh-uo%P
zo?x&15R6MB$RcmFX=wd9vj-4NPW@z`g+5Q@nv|Ez=Doxar^h3kNXskd1o1!jUv4v)
zkLH?V(c6fE2y`U-nH2oFt`?C>7pi1iyHgM7-pP>>5lD?|3B>gJ*?*hMaDeV+$r;@B
zY`^l{&>$hfbc2yi8d0qVP&ZhcAt$7Pu;%KbA~r5vEse#KipkANCVsG)v;2$YyRD6}
z#UYPNAWCq5U#6N-6&2p7{`jn@_H8bGE<2zmup&6U+Pv2k_$1tA+$z%Ww4BX=@_sbb
zoGM@=D_mB|(C5*vB~hWN1JY%p&)9vHNdL&A&??ZUAf2w3PaoyuF$+Yg?!<?`Ie@f+}P>~@=Ea=-w!d?pJInB%H3-uViQrGsnnR7U@30qN~v
zkI!wfvv@sp)eTbVr@fLoNM%U-hs1B2_BK1i#>kosrhJvh;pQhr9k0&QW2#=*Z2tW8
zYfR0jcjDyhUsCS&b8lxovgqWnd{}|*JRZ^#2w--T_;gVYSevL}(7eIK^+!Z_=?^>N
z9z@=oE+A()`m-&$3KxySv{rf|I3JCawGHD#O_%}HTltc8`dfJ_cc&4R6K
zrjm}1U@)WEt{lnogBW{y=UF3^eE(t{ME>6~kJ%7`_i^JHW{l}bY{OB+gv%(WkvTOo
zX+8AY-*WgMPr{SlLdN0Bj4l4{N9n;?6G?82@*b|Emb_mK%qIy|Cl!o)vt;njyEYbA
zw%3iHQ~hm?z|<92HZ+Z6WTX>TEX%5>f1+*Yyup#pYlvEv6c
zo042H*bJiY3Oz-<_YCCL6;r1wD#j>~49tJifr7P*kJzike@o#Hoji
zGj{8x(cU606u}K0h*{}o>4BmVm`irfDM)%RAHQ$ir}bSjXc3NQlQF#J3dk#O3KR|R
z$iIcUC8t}bZ5SpIL`nKStqrU#U;ON+YP0L*%p-r-kvI0@tGSEdY6AF!c4
zh(|B6zvShVYu4kdVYcs|1)k_AyCu$r|GFkx095k$SIjenA8Hrx@XI^qV+gEi?^dPV5VaRls%`EZ5>1|MrmT7eNCow
zA+;~KJmNKK9E6~sv|9fSiKmyI{p~niHN6ypGRLq_v|M+%e*!0F8u4BY37vz>>`Oq)o
z&s;Du+?99$`(SQM>RdvLuc!}2&JURO**@0$q6kH4UGmUvUEO+Gh{OtX6*$8xWX}&t
zUdrmzfyj2nP%=v71FR4Hb-uSvR+H6U3B1`8q@5uM3
z!nF6(T1yhm!Aas{zY=qgM9dL!LvoCz&&Afcp}e{4W&)zXT4lX8ayMs3hf6~PSkKhR
zf+SH6zPZA~t(sOWmM_@X-R*nip6RA@Mx&DY9)@~YUcS}dDdlGpGg|=>WHuMvVbl7@
zy(vMvb3edi?(3@O3r-#=FH$|t+QuMp
z#YZn({p+i{=Qc>!%94X&4E=c*5eE>;&)EPB*?yJ>--#%&tUH3C9X(y#&xQm&;Vs=H
zjuZW5niT>f=>P|L$gdNiTwkXCj4cF4e6__`J{zZ1`6=W-5g*msL&@tNSS#JVA!|Zm
zy>Z?v()~_3JCChl%HA{57?-qi*s4%LVa76i1tu(AIQVl`+t#z{Nkfl7G>k~y)YJx!
zVoB{e-g>Q_aDN;k7l@42ptX#Ny>Zm_DbbyDgJ%#}vq~vrM}LvvpwoiD3nItlKdN;-
zDZTWN0BaiEAycqQNqMt8cs{;LsU60Du){XR6H;I_b(FpKP#lZ0;eFQmP|k6I{3=(v
zZ<6NFtNMpDP71g%*!bn{#|$61ZVh<)s`5oaU7z%E#mss;g!Q@D
zhhE_w)WdF2-d{}3>Gb$X_Q4m|^!xlBgv!&@x&2c%9KUW#7U>@-Yg56Ur-FFB4fya(
zN8UD}4!?aWLG}hK>9sLxW_xD7nx)MS(h}|@=d_yRfcjKuNXCoP57)(a9$rmJd^sO;
z^ov-x!{vR-7tKpMt)bulr?FMwaV+X{88vxm)1nM7B4d?
z^Yq^gpUOb3rXX8B95i&sG;Ls(c+@FY@*`sX>##ymBle;1AAl|WF2%3{FR+W|$VdU&
zwA44$xBjEg6PEb(!yBE~$D}Uve%)z|k$yize4lwDC6I|xJnDO+tSSGqI8^K8xRT|h
z<*0@JVxYQ4X~Ql@>U+~ZJ4dIB8nzwrD%_)6cX4*sMRzU7hH?nU`L5ln>T;_$ynq(0
zLQts^@x^^Vk0|Xd2JX-I&fx<6i_AO2OUn&%HaIsORkaDzsX=ZLVjKJ0jTt6Epe@c;
zf}nWpzoC(uxdvz?6z3@}-sI`UapvpMXKC0L5n?vF-@eUfHv8{!kL^dTYfXZzd+eg3
zH}7g)h`LG7-|B0iQY*n2!PKxCkSgFYZC#CE8;VB*d0~MwXt$t}*Lxu)CRYii
zPICZqBl3Fp5?aX~^u)~l
zZ|DqFlkJLDfNMDcXV#d<5;Z_ADbp!|XuhD=^dg4?Yy{cz0pNnOkTyji
zU$3er$BN$SUgEOT3zW)hZ<837q6+ReHYZ|RYTTdTKG%G5oMTg=6Rlj@d`*R!OXCs)
zwnKm1oNE)ln4tTQW1p?lcc~j*Tf{+O()7;GwaM#>TVqR&oA`E97SR%b+G0MS-jked
znJ^*$z}-52>Y;7C$O8(?EydR#_)3vaLmh^7?k!(t51?k#)o>-#octD9%O5fJ%751bcWWi#S#j5^kko@O^nt03N!_`R3=h_~8rNe!Jc2n|&v!PJ=-^S|e`wAWRytRTzyUym9dOW<6=lY4dFm-+OBMt^E28
z1#lsbv_qGHjCL16M<5XOQ}gJ9+uIy@8A9{cQ83{u*)smhlZqYH19I;GsMw?V7xi&I
zw5W?}5JMzArCh-!n+l^(*7_a^BJ}I}l1@5;qjXOAI}!*-8@t+9;}V$?1^4WjOsh2X
z3Nuue?E1#%Sn^y$Z?1h5+0Rcy41;jHp-fa$wJdU>NPBcf7~3^x>k%luN+cPCi`yS}
z>W$aCZiZx=O9SU*$uNW(&_m}TK$|C#^M>P7Uhg|EeO2rjLfrK!d2(fOl)Xd`^QIgA(`4P4JwU
zbxyKlZej4|#(byd{G$RCbTn#8UQyxtxDB_priwQD>tH}@kwyWahm;hxDdJswZBCEM
zEIlXR5N9M8t%A|pBnJ3#&NSG9g)pe1zY}h{PGlTQ+b|otYKWH{WD&7cs<+9})3^;t
z*_bY9y<7WHyodE`2J`*)HozX|y4wu6zV7Oj*m6B{srPO>_20aG;OINkgsIL;oXo8T
zQ=#_*KEI3&;t|+NFe@T$45WkDiNPR$TjxzVv6%
zBVbHRM-UiIk3e8#T1dT~H#CmM89F4!jDhkGmgRU^7bqwJYhQadQR`l~EuU5$CSoHp
z?!@M%2WY_^)c*=a!aD9Twte8)^o}c})@R{6u&|*6{7ORK-RJmZZn@c~R=pn7J6sPeMmC?#a38ShwSN%TAjwm
z?-9+UT(=h1e{ANDIj`}Qz5?5Yiz(0*?YO(dFyH&`)8!yXF*nw9ileqYGRq~HzJnx|
z*@a7|*48JjYpJk4Wq_|(q#pVI)uA+55g3uRF`?>f*W|^N>0x*j5%jT1%+wB~fIM3n
zUlU3^w*bdQTi#c@q`NZ=#2bkZ5~UH5&xUib6yd^`(M
zpr_7SaPL0_u7XFZqiU~Qi5@%*VNEnG_Z)cf2N
zN5*2YYGPbc6-SsE*L?4!gd5!>I}hY@wV3)YMwUNI-8>6c;|-abP@e(o)!gd$1BCSn
zHy2Nqojc60)I*dMP*Vw)Add7zvZVH$cNA0^%Su?eHyP
zr@O~dYdlrq@b2_+hfi=4ZUOmMgKvcV`o}q6^^!SXOfz@-yxvhs&4xDcRp3B{>MGpX
zhv!?p)2sX!E@jYovM*2d)%}bWu4k#Xvx=%iQpY0FG~_%5-riLac5Rby%WHJIygFjB
zAMn;I?`vZK{Xd1EhwVX5YiW=u$h$oWu4nq*lIggTE-6y)*9a@Nd~Mp!Q6}$U1kuSx
zBV$1FMxD1U2O=kxk|_4@#qEY0eR^V@v;D(NX9Sb3sE(;Kg#9!gap!fFOfS88W$#LMdEF)cBJWC#)aI{e&SyYXvBsW8&{)LS9H~Bv
z!5nKC{}<+MVzSRdKATLxkT|C_pY=1G|5AXW=XY6_LS%QTMG0kkwRx~BQpP2aW%Wk!
zw@zMN&aCs(-DiCxZpZCoua*-SX#P-ea_k>OVBO}lH(s&ffP#|As-_5&`ov;g&B`0s
znxhO1Y2+m~2%>9ccs{K1wE%Dg7T@J!pty8W^k;n$h^WGB)#l{5iYm|=zTvsU7G*H@
z2y$EBN89CIQD-+V$FZ-Ww;?#vdH!XBiJqLXk!aYMTdclNDvn<&Y9y(7SuIcMZoRWr
z5h{6VSGAR2TjeSkEn^kx?yv?Dd%bFYc7QE2a5)^a_*t-qkXwjLJ
zK}GqbcCpEc?vJ7?mABUH@|k-`h~D!I{AZerxz#E&n$izidj&
zkx2^(*y5t5m&m-l`GAEv)w&_Ao2K29zmV_BF*D8G%qh2X=_yM_>rsc($wZfznjV}X
z{6$TxTSIRr_Xo?cIE+=!W5Mp47ac8O74tE=(iPi#0^0GnrpV8oJdZ4SK0H!9`kQIs
zvZ0tR-RZJ(H|SH5_jwz|4XDNBT4wgl&ay)M{MH0+H)z2r>QU2;n2*<@?gy8SoiapE
ziV;s%0yZheA%N{6A#3C|WZEIzw&+H=ZaovbK^`eL6>AQNCv=qb`ra6sNPAN47+V~!YtKPHgJiGJ#
zPT!$`WsK$TLffdzjThKc0#r}`c)g+Kqm5ipi3}&gYaXo|^?%o(UO-$9^nQQKeBi49
zhW%`Zo@C5aq5E@rJ96n{kh-~|Im9EC6_*mU%y=!ekPO{;cgpI3sJ|`ku5zZp
z#KZ6B>H8_bF1o)e+?i%bL2E#giym+2_)Z06m+d#_p3(s`PgyD%nlQhjPkPUHF})}u
zU#CZAF>z60;7|{&n9hfQ`e&afwwnU^^#;x@8zYlD%nVhE!EX_}cC_Ks8E`{pGO=@<
z3%$o6VT%=RHDFFZPBrg6xe%hxIWd{0Zh-rGri*~}TG*rszmIyeE=J_KnJ
zbiWJ<*GGtMYhbP!=c;*4@{K5*NwI?#@Vk@s7;c_)Sbfe4s``b!-3!+GCrpmPj;?e(
z@%8)`B?Wx8W;l_hq*@p~`!;^i9O^HeHUrZa-wX1)8{`9|8G@y+!>r8XM_4L59KXJyJ=uiLK
zCx`#xWl})iLffZ^hWCb`b9ksq$o^h
z0`rL$S|*hd1}Rjim$!CW=3iHg?-G{h7&$nLHVbDsmTlR1TIzAEqss3}>@N=B
z&devY1r($}einv}-6!>>vQ@`be}}kvIq8dQd8OgP!l0xl*$cBK@UC+^hIKw4d8CZa^xne-xt6va@7cv+6-O@=
z4}b4yuBsC9)KaTOi^6=;10vX{GlkoiY{++_<>=*StQr6>Ze^xbky!AxZ0~?I@$xsV
zp}Q?awd%-Ebs$xf2iNpYojbjV%E-v_$uzzF?xK809KS4eWB{fRU4OSbGq;#&eSM
zyTv@BvD%={n-S+k_4LuGbS|@VrMrX+7_T4ZCZ9V9O;1gf@Bn9~!Ml^CH{GTUnE|>TA9#nwa_n&EQ1D+&0Z9IXP=+PiWm1m$xX7w_-RqOJrVJL;4A
zGR(qz$-65n>8{3yNwaCpEdU6)y=M9jl&`ki!2XAC{jf9h
zzl3+M@|ao=3xdFtq+E3q>i0~8Ts;DutW_!Sz*fIyEE#S7kP~oL(WN-u(3{XQw+t92
z(+Hy!-*p<9A@a<{$18!|sRhPebw9JNS>llU9ckshHNr;8PLS$wv*x9Y6T`)1H_=AQ
z(o`ADC;l5v|4q$Itj`uP<}}!3erjCZ6gg!0J(uT3+YyDY^;Y!EH7Tj38k+O*4BoCy
zpPWTWPIl((R2nuF+Ewd!I*=!R!)(!SgYE9`9wrY+J7g
zBQ$bbsa`p>3;UodB=g`f6ffh?x~R|!P#95_P@%b?Vv*}t&V|NOB#_oFrNP_h@E45fqP^5}8n5rLbu&v0yaqy1Fo>IKjf|soYSV$~e!aJ)
zn689g)RrS7-Q8t?#@sRA@Jl1l6B2RM@*HNR5`DgsMFdUr
zEz3nQAT3?R*-6N}(i)-72h0;_r~F2j_1rQZ2rDMsc?`mu?N$rX7&>+1Nn|&{OED=h
z6tZQC&wpAD=D762dG*MqaDaAj@KQCZ?a1_L^Q`gRVrfA0N5m#-GhB@MhqLZx@ne?m
zm!OUCzV~2|*{(=2!D9GF1Zc?Aj;>c$w#+^n>
ze}+H$^1>V=@bwn0lyH5I;k2?ocXE|#k?DVE?*K~~@I~*(I8@L(4p28d7y;|))hEe^
zpoOhc_C-+PcuYtyvyRFArg`s)%tV4627Ip5sDeNyv{qg+slvWcM`X^u_`l4ob&$KYLTXDqE%*iqLk7B&hzPa~!rq}I#LEuHN6Gusi
zD8u-~#n#QF{XH*HxBP{j34iDYQE6oSjli~STn~@q2gg;}w1C`7-}}aiW{3=~Ho1!%
zBT{^x#Dt%bZ#H^r__Rof<6WnP6wU}9;C@WCZ|gj{v6HD3KW
z7{^72P|xEU{tL7^;)cp1m^KsUnWNJA&^I|hoqO4NMCdyo
zr%)|f?yP%G@Z9ilVbi1Lyo0hzxgh)f-xZVhMe|;2ab+PqH#6G82*0*HK->c9rCj$H
z7KeFzZx2qrmF%?xDS@8i^Vt`4Bac7kR(BO`rSiRv^m3;r8>6mvkr!cleSa0CaYBa|
zb7w!9?u^{gpjdJTIUMTu$hbNCxGnFG!)pFiy(*L;(l8&ZQMV7tnU1cU5qZSEYpbDq
z)uJEcR=dfpw_<=A0h{5?l6`{WW@JfuadZ(njLij0@%Ekfs>WQ6j-9lPzO*Jt_^?7F3Lw$12z=nl3|RxX
zl|0{1y{kYUFIZr22<73tkY6BNB=YolooxjGuD`>Nj^4$x~j)!tkD8Aokzu9k5
zY_my?kmOpx_3(b!JUS>K@ZE6JBJTPI6l&Br)CSZ;nuQrHEY3AuOw>|y{~>yKbFF0L
zn^LvIz~BtgW>rRlWBWMQ>}9U4s`nuQ#eA(^)wz)84gDYjlJQW2`1O&Yp0H#In+R
zoEp1M3wlM(oDyMTLhRi~Dw7MXpmqXqxupOJyU6BHLb4_WXv9idV
z-6GoP?om_pYr!q3-n-j~e6J~aO2)x(n&8Lf`5e8!8+FTw2R}Bh?YvM2J~}&~wiyX`
zq|awTqW4dKSqyh9>AmtFN@i|zHm0if_zgZ7d8=!-T%EXQJ~B?*&BN_O$%2o~=K)F?
z8pB=9RJj0`6LMr>g9oU%Ie%g*V
znHGAqS)lQRO3ur&p_e5SgYdIO=0TliHDvP7nM);`|ge*{;6Xs;U=%7-!
z`p@bR!1c=e?S#F;y5T4V<(){bRCxF<7SJ6XouVdM1VvI!y)&^-X2=YrYedP;)8~g`
z8Z3*6KZUt8?}g^n|Arm&26jB2Nr0R7j?949s0ZXN_sKxO`eG9Yn`eRdM@jtG`2XzV
z=C-zNL>5<`Hq6Li2>^U=a64&kK2%a2B(wb2W)m@AV+9mqot$OqkTT&u~6yw!+5f(CDB`a+qY%F6?Uvt+QSW5}*Y9Nfi&1oUt@9~rbK+T^hL)vy=
zjGHVQJQ}6DCw6x&sDD6zEN@)>3Y^f4!d&hCU0IcPbN%O!4vRX46hkfqxsbibIN9p%
zmPtx=aL^!Yb1>^#=gBkfmf9t2h``evy)SA`&yQ!KxvV-2Kzha8vlU!p
ztLZcS(wrJg>&`ZbBP4$B7N5l~a@xpLR<)Him6WSiORLry>}a~+KM2x@ReBy&syv4K
z1bWfz01LO*wU7ITPrRm*gt%TZ_d0k+wG0{c1e&iQq{9?;=siWeGv$z7gM8Jq;v+LLB%kynFyP?!`N9s@%L3uq|=)bCi-)B*@N_C
z!1;`)_XB)N7uSOOBCLcRQZ;t*H)b>Sv_Fv@l+&}=8P#Kr#jfi3
zi)S^>XG&W_K+E5Un&k+e5=^p(tWsVCHd)#lEO@$`Ef@KT82;rJX%HhTfdhuyvM!Js
zK-0?0v4O${9Nsou=M5`=xDi?Jv*h{HF8-RMy2%D)ez#h1394m!2cEe6sz2ZEK+J$1_1{%$3JzD{)JpFDwfZs{IuA8nzBhb9V!f;%DeZaEp=|A@}O1Tmj5lx
z^b{^`moIP_9v3gfE+cR5GLV`$-0$
zS<~6?(qMNcfd{v|ycv1%rXL+zQiy$L799v(l-!Si_bCl8;Usnx*!z1y(7D^{KA+vQ
zl`06*0UT`)7%A#kv^?xLn5Q!M4HjCtuRed(xY*+0_=M3)zK|^V3wlwB!SZnW3|#2G
zUL#c}4<@+*7%l{h^+Hg>zerN_qHyv1dX_1T&08fBHg-NlsI61+aqjbFh$6FGz1ro+
zteHVP3|uN@E&uiL^QcTck9~T+9GL$6FEx8d=iI
zs^}fOZ%%yfb?MY5C3Q*@Bn^JTzU75k7e^6#FG%VwLw9AjD*$Ze$b8i(v)RH<
z-n2Y4#D>+6L|Dwld^hw&VTEO(6`dr>Uj1M=63~*tbP9%|@w}jzb;#(F@TZ`w?;UAp
z#SWv7PT3MPk4^FtT%eb6fh@gO>sz6*_Rxc+Cves4al-r;NNwXc^6^@P5ChCoo;;UH
z^T_st+_G)Eo7F1FwZW1pCzcDSgP84K44
zZ_ddWH+1Tz#@%$kaEJ_1&spe^B(>E0g?}d0|EY@Jky(VeHy^9j=lbuYzNnmkGWEAO
zWoVkJ>b@L18Z_iebCp_h=~L|tn0Qcd?Srgug{hLdIDgG?0S5w!cl-EyE+EP>_@bAh
zqStpm@wbjs^%_}}4N+a5xdC;6Y1WA*YzfiTpER3)iTjiWo3dSMa~vUE
z{R~cTqM*#Utu2TCt|NS!9Rv;b4SSZ0jF($}Odw1hd|0j544v1Jwnc$iTC44gnFf$_
zbbHX%00I*|mD7~yY3P39k4`8A$ItF7+pm~eBS-uR*xR+3y84Tt>{JnT24r{@E-+yvlp;p}{)3c_|)yz)1Wuo(^t6;%GJe2m$)zvjys)x{>&
zy}(_C^_z!)YSN+8>a9P)7v@K5X6i(Ig%e`W!wdV+nT3x=&Bn+6I{{`oaN@2^3DHON!RiMv~HQnqcK77
zYI(xb!O)uXLWDlPY;x<_^1cN{KgSb*bc1(kJK@43w-nyJGDSR}vPVs6v_fzG
zNzvj@TMl3rDhrVldxZA}t)NehzxMq0stKM$U8kNGjZ73@`1)O*0@)&9lUy$fDYkTO
zeJry~2+gqAxT$!<9}Z?{cximl65JeYsiW(g00V%6p2Cov@=)CM+o!35Ye#O#Zw^W;
ze3z@NJ=aT=3FDx21S(la(*Ie(;^8HTQHC(4MNai%$LwC#TiZpoABW11im0TnDTa7N
z3+dW7=4tRDA0J%}5rIBTuNEGzI~gW)iW&DJ)q*OB3WoscQ!^iqtv-EOn%TMEs&;c?
z0v>w4GBG$Tu2XpPqzNK*7^
z#Qxd)siRePk;6qAi%QGefPo%t>vOqo+J#r8&k=M=DQCyG4rgyIP$!z}P5dsiUanl(->9haMOHfsY#2l~-oiSR^
zD!lpMRjN6&M$z7+>k${G40}mo0fjXpf`ek)^vGI~NVXr>p|iM~FFz;sLM+Wt
z9bALPH&;ziL$6l;tf4fF6-BPibE!S+Qn~byd3W1YTB;k)X%}-+B8(#zKa+cH>9%2m
zuVVEJ|7VW@mS{Pz=HVR0R_*|NbI$lwB`NoEjo&dt^;7*-Yn+$aYoV}_zM`<P;U7
z@>+#sbK=N${$u-Owna*{e29f!RGORQ(PZd2HJeJkbX{Y+y{e3ei&e@@9)0PBlWiZZ
zlw6s&{m5gh8K4l;@4poE?^nNfk-4i^R?WCOiG1Vycw_Ojq#pI8fqj6)^2r`WTz$<(
zmWC}^-Q%|Nfr^5crj-0S@p)2~y&$
zK>avYhvE=a;0;dwp48sTU0O{cZ+5k9p^!~!h}0&c>s1`)fJD9yw)|vxMrRBf$nw2i
zI|4;?o1X1gd&?R>tO*q%1zzs}9*dK4;#iq$X7#ooD>~1(BDQb82YrKx
zjmXzO)7iIlbz#zvAI3(BMtWa4lW+{`8DIw58M`zTQb%}HHXq)6HD=0po!1BIcv38t
zNRVixu+9fD#!s?BoiH==Owu#ateJ6h!?ToFn5FcQzPd-YB%6HYjLDoxuXOF^)^2te
zPih--ITh}c5wp8XpUXYZWR0GohFUH0t}&QQ3!j{yef9`n0E1z;D+RQ^&xX%Kj`9$F
zt@jauj;cPxTV^{^SBP^ve|I5D=-~6ahjIsFn7uDMhR@Os=RPf(H6TAIpAqJ`oNx!c
z?B)pt?S%A(8paD>@e^*@;_hl-Eqn?2xDAsyyix=|wu3N)GI=%_MMcg{pl85}k)y`k
z)RPXtSJZtm^eX@QmIhUzw;DdR8iH3ss?g}0?afkxpI0h7zxmW#QZID_7k&Kv1}}s~
z_C}(4YLiL4D2@3nHXhe@7$oP_ei^Q**hNS3#6qtUHlEms+OphINX~1t%pycE`8|aV
z7`e0#nx+dUHrl6v$zSbzYu_j^f1Y{;#-kShViSYV_B_0pN|klG4ZI|tt*pTpIB
zi2|6$l!k8lZYjK&yHSlDR{YK!3V1u5EU#nE`{36zQ-h>91)m5cL+&2EtAH`DC^omL
zqvernk?9e4l(8*JmcxnIuPuBfaEA+3q(E02zXDH9vLy)XKg(uz5w|Um7K%e>?@VWc
zQ*@``3Vi)X2~KeH?j35m5mSes4R&?+==!kl59#!Fq5sXJJD-pAL2g|=EWNMX3Af&>v82@O+Yz|ybYY=`6>pDJovY~rI
zPLt19mva0B#vD^-Unl;BH#^a?%U|$kerV>jqvhC5W+i*6<471)z(GWj
z%kU((NWxi6wjk%B-|at?>LyPYh5P&TLABRtCbA+Ho0>-te{Loj)>_UK!Y)DQr~3e7C?Ymw1{@W
zH-i^EgnN<3L)m0zP@oxDlC+p>#j#+-@w$$I7k84Z`aF$$BxrYYnjKx$-W236iUY27
zl?IHqrCt`E#VIT438=4KdLu7_f)!2!eQP+Zi+xKa4G0AUU()St$=TmWDdK`*lS>vM
zCBK!WQoVzVagUf;ol_$Ioa<*@?@OMMgnGv35ob)ee6+V`=D|;AtIuzY;u6$LoA