Browse Source

修复客服认证、身份判断和双向沟通功能

pull/1/head
Default User 3 months ago
parent
commit
3626c14372
  1. 37
      app.js
  2. 7
      app.json
  3. 44
      custom-tab-bar/index.js
  4. 18
      custom-tab-bar/index.wxml
  5. 27
      custom-tab-bar/index.wxss
  6. BIN
      images/生成鸡蛋贸易平台图片.png
  7. 161
      package-lock.json
  8. 4
      package.json
  9. 17
      pages/buyer/index.js
  10. 1439
      pages/chat-detail/index.js
  11. 8
      pages/chat-detail/index.json
  12. 102
      pages/chat-detail/index.wxml
  13. 468
      pages/chat-detail/index.wxss
  14. 474
      pages/chat/index.js
  15. 8
      pages/chat/index.json
  16. 43
      pages/chat/index.wxml
  17. 182
      pages/chat/index.wxss
  18. 165
      pages/customer-service/detail/index.js
  19. 7
      pages/customer-service/detail/index.json
  20. 93
      pages/customer-service/detail/index.wxml
  21. 317
      pages/customer-service/detail/index.wxss
  22. 362
      pages/customer-service/index.js
  23. 5
      pages/customer-service/index.json
  24. 89
      pages/customer-service/index.wxml
  25. 383
      pages/customer-service/index.wxss
  26. 591
      pages/evaluate/index.js
  27. 138
      pages/evaluate/index.wxml
  28. 21
      pages/evaluate/index.wxss
  29. 2
      pages/goods-detail/goods-detail.js
  30. 2
      pages/goods-detail/goods-detail.wxml
  31. 131
      pages/index/index.js
  32. 16
      pages/index/index.wxml
  33. 7
      pages/index/index.wxss
  34. 326
      pages/message-list/index.js
  35. 4
      pages/message-list/index.json
  36. 30
      pages/message-list/index.wxml
  37. 101
      pages/message-list/index.wxss
  38. 38
      pages/seller/index.js
  39. 2
      pages/seller/index.wxml
  40. 382
      pages/test-service/test-service.js
  41. 97
      pages/test-service/test-service.wxml
  42. 249
      pages/test-service/test-service.wxss
  43. 232
      server-example/package-lock.json
  44. 3
      server-example/package.json
  45. 125
      server-example/query-personnel.js
  46. 394
      server-example/server-mysql-backup-alias.js
  47. 960
      server-example/server-mysql.js
  48. 79
      server-example/test-managers-api.js
  49. 138
      simple_chat_test.js
  50. 333
      test-customer-service.js
  51. 96
      test_chat_connection.js
  52. 1276
      test_chat_functionality.js
  53. 75
      update_product_table.js
  54. 110
      utils/api.js
  55. 492
      utils/websocket.js

37
app.js

@ -2,6 +2,15 @@ App({
onLaunch: function () {
// 初始化应用
console.log('App Launch')
// 初始化WebSocket管理器
const wsManager = require('./utils/websocket').default;
this.globalData.webSocketManager = wsManager;
// 连接WebSocket服务器
wsManager.connect('ws://localhost:3003', {
maxReconnectAttempts: 5,
reconnectInterval: 3000,
heartbeatTime: 30000
});
// 初始化本地存储的标签和用户数据
if (!wx.getStorageSync('users')) {
wx.setStorageSync('users', {})
@ -47,6 +56,21 @@ App({
}
}
// 获取本地存储的用户信息和用户类型
const storedUserInfo = wx.getStorageSync('userInfo');
const storedUserType = wx.getStorageSync('userType');
if (storedUserInfo) {
this.globalData.userInfo = storedUserInfo;
}
if (storedUserType) {
this.globalData.userType = storedUserType;
}
console.log('App初始化 - 用户类型:', this.globalData.userType);
console.log('App初始化 - 用户信息:', this.globalData.userInfo);
// 获取用户信息
wx.getSetting({
success: res => {
@ -102,7 +126,18 @@ App({
globalData: {
userInfo: null,
userType: 'customer', // 默认客户类型
currentTab: 'index', // 当前选中的tab
showTabBar: true // 控制底部tab-bar显示状态
showTabBar: true, // 控制底部tab-bar显示状态
onNewMessage: null, // 全局新消息处理回调函数
isConnected: false,
unreadMessages: 0,
// 测试环境配置
isTestMode: false,
// 全局WebSocket连接状态
wsConnectionState: 'disconnected', // disconnected, connecting, connected, error
// 客服相关状态
isServiceOnline: false,
onlineServiceCount: 0
}
})

7
app.json

@ -9,7 +9,12 @@
"pages/profile/index",
"pages/notopen/index",
"pages/create-supply/index",
"pages/goods-detail/goods-detail"
"pages/goods-detail/goods-detail",
"pages/customer-service/index",
"pages/message-list/index",
"pages/chat/index",
"pages/chat-detail/index",
"pages/test-service/test-service"
],
"subpackages": [
{

44
custom-tab-bar/index.js

@ -11,10 +11,11 @@ Component({
data: {
selected: 'index',
show: true, // 控制tab-bar显示状态
badges: {}, // 存储各tab的未读标记
// 记录tabBar数据,用于匹配
tabBarItems: [
{ key: 'index', route: 'pages/index/index' },
{ key: 'buyer', route: 'pages/buyer/index' },
{ key: 'buyer', route: 'pages/buyer/index', badgeKey: 'chat' }, // 聊天功能可能在buyer tab
{ key: 'seller', route: 'pages/seller/index' },
{ key: 'profile', route: 'pages/profile/index' }
]
@ -145,7 +146,7 @@ Component({
// 跳转到鸡蛋估价页面 - 现已改为未开放页面
goToEvaluatePage() {
wx.navigateTo({
url: '/pages/notopen/index'
url: '/pages/evaluate/index'
})
},
@ -203,13 +204,40 @@ Component({
}
},
// 更新未读标记
updateBadges() {
try {
const app = getApp()
if (app && app.globalData && app.globalData.tabBarBadge) {
const tabBarBadge = app.globalData.tabBarBadge
const badges = {}
// 根据tabBarItems中的badgeKey映射未读标记
this.data.tabBarItems.forEach(item => {
if (item.badgeKey && tabBarBadge[item.badgeKey]) {
badges[item.key] = tabBarBadge[item.badgeKey]
}
})
if (JSON.stringify(this.data.badges) !== JSON.stringify(badges)) {
this.setData({ badges })
console.log('更新TabBar未读标记:', badges)
}
}
} catch (e) {
console.error('更新未读标记失败:', e)
}
},
// 开始监听全局tab-bar显示状态变化
startTabBarStatusListener() {
// 使用定时器定期检查全局状态
this.tabBarStatusTimer = setInterval(() => {
try {
const app = getApp()
if (app && app.globalData && typeof app.globalData.showTabBar !== 'undefined') {
if (app && app.globalData) {
// 检查显示状态
if (typeof app.globalData.showTabBar !== 'undefined') {
const showTabBar = app.globalData.showTabBar
if (this.data.show !== showTabBar) {
this.setData({
@ -218,6 +246,10 @@ Component({
console.log('tab-bar显示状态更新:', showTabBar)
}
}
// 更新未读标记
this.updateBadges()
}
} catch (e) {
console.error('监听tab-bar状态失败:', e)
}
@ -233,6 +265,8 @@ Component({
// 初始化时从全局数据同步一次状态,使用较长延迟确保页面完全加载
setTimeout(() => {
this.syncFromGlobalData()
// 同时更新未读标记
this.updateBadges()
}, 100)
// 监听全局tab-bar显示状态变化
@ -268,6 +302,8 @@ Component({
if (currentPage.route === 'pages/profile/index') {
setTimeout(() => {
this.syncFromGlobalData()
// 同时更新未读标记
this.updateBadges()
// 额外确保profile页面状态正确
setTimeout(() => {
this.forceUpdateSelectedState('profile')
@ -277,6 +313,8 @@ Component({
// 其他页面使用适当延迟
setTimeout(() => {
this.syncFromGlobalData()
// 同时更新未读标记
this.updateBadges()
}, 50)
}
}

18
custom-tab-bar/index.wxml

@ -5,7 +5,9 @@
data-path="pages/index/index"
data-key="index"
bindtap="switchTab">
<view class="tab-bar-icon">🏠</view>
<view class="tab-bar-icon">
<view class="tab-bar-badge" wx:if="{{badges['index']}}">{{badges['index']}}</view>
</view>
<view class="tab-bar-text">首页</view>
</view>
@ -13,7 +15,9 @@
data-path="pages/buyer/index"
data-key="buyer"
bindtap="switchTab">
<view class="tab-bar-icon">🐥</view>
<view class="tab-bar-icon">
<view class="tab-bar-badge" wx:if="{{badges['buyer']}}">{{badges['buyer']}}</view>
</view>
<view class="tab-bar-text">买蛋</view>
</view>
</view>
@ -32,7 +36,9 @@
data-path="pages/seller/index"
data-key="seller"
bindtap="switchTab">
<view class="tab-bar-icon">🐣</view>
<view class="tab-bar-icon">
<view class="tab-bar-badge" wx:if="{{badges['seller']}}">{{badges['seller']}}</view>
</view>
<view class="tab-bar-text">卖蛋</view>
</view>
@ -40,8 +46,10 @@
data-path="pages/profile/index"
data-key="profile"
bindtap="switchTab">
<view class="tab-bar-icon">👤</view>
<view class="tab-bar-text">我的</view>
<view class="tab-bar-icon">
<view class="tab-bar-badge" wx:if="{{badges['profile']}}">{{badges['profile']}}</view>
</view>
<view class="tab-bar-text">我</view>
</view>
</view>
</view>

27
custom-tab-bar/index.wxss

@ -66,12 +66,35 @@
font-size: 44rpx;
margin-bottom: 8rpx;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1));
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* 未读标记样式 */
.tab-bar-badge {
position: absolute;
top: -5px;
right: -5px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background-color: #ff4757;
color: white;
font-size: 10px;
line-height: 16px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-bar-text {
font-size: 22rpx;
font-size: 28rpx;
color: #666;
font-weight: 500;
}
@ -134,7 +157,7 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30rpx;
font-size: 36rpx;
font-weight: bold;
color: #FFFFFF;
text-shadow:

BIN
images/生成鸡蛋贸易平台图片.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

161
package-lock.json

@ -14,9 +14,17 @@
"express": "^5.1.0",
"form-data": "^4.0.4",
"mysql2": "^3.15.3",
"sequelize": "^6.37.7"
"sequelize": "^6.37.7",
"socket.io-client": "^4.8.1",
"ws": "^8.18.3"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -312,6 +320,66 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1237,6 +1305,68 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@ -1365,6 +1495,35 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
}
}
}

4
package.json

@ -17,6 +17,8 @@
"express": "^5.1.0",
"form-data": "^4.0.4",
"mysql2": "^3.15.3",
"sequelize": "^6.37.7"
"sequelize": "^6.37.7",
"socket.io-client": "^4.8.1",
"ws": "^8.18.3"
}
}

17
pages/buyer/index.js

@ -31,6 +31,23 @@ function formatGrossWeight(grossWeight, weight) {
return ""; // 返回空字符串以支持文字输入
}
Page({
// 分享给朋友/群聊
onShareAppMessage() {
return {
title: '发现优质鸡蛋货源,快来看看吧!',
path: '/pages/buyer/index',
imageUrl: '/images/你有好蛋.png'
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '发现优质鸡蛋货源,快来看看吧!',
query: '',
imageUrl: '/images/你有好蛋.png'
}
},
data: {
goods: [],
searchKeyword: '',

1439
pages/chat-detail/index.js

File diff suppressed because it is too large

8
pages/chat-detail/index.json

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "聊天详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {},
"enablePullDownRefresh": false,
"navigationBarBackButtonText": "返回"
}

102
pages/chat-detail/index.wxml

@ -0,0 +1,102 @@
<view class="chat-detail-container">
<!-- 连接状态显示 -->
<view wx:if="{{!isMockMode}}" class="connection-status {{connectionStatus}}">
<text class="status-text">{{connectionMessage}}</text>
<button wx:if="{{connectionStatus === 'error' || connectionStatus === 'disconnected'}}"
class="reconnect-btn"
size="mini"
bindtap="reconnect">
重新连接
</button>
</view>
<!-- 测试模式切换 -->
<view class="test-mode-switch">
<switch wx:if="{{!isMockMode}}" checked="{{!isMockMode}}" bindchange="toggleMockMode" />
<text wx:if="{{!isMockMode}}" class="mode-label">模拟模式切换</text>
</view>
<!-- 消息列表区域 - 微信风格 -->
<scroll-view
id="message-list"
class="wechat-message-list"
scroll-y
bindscrolltolower="loadMoreMessages"
scroll-into-view="{{scrollToMessage}}"
scroll-with-animation
>
<!-- 消息容器 -->
<view class="wechat-message-container">
<!-- 遍历消息列表 -->
<block wx:for="{{messages}}" wx:key="index">
<!-- 时间显示 - 条件显示 -->
<view wx:if="{{item.showTime}}" class="wechat-time-display">
<text class="wechat-time-text">{{item.time}}</text>
</view>
<!-- 系统消息(如诈骗提示)-->
<view wx:if="{{item.type === 'system'}}" class="wechat-system-message">
<view class="wechat-system-content">
<view wx:if="{{item.isWarning}}" class="wechat-warning-title">谨防诈骗</view>
<text class="wechat-system-text">{{item.content}}</text>
</view>
</view>
<!-- 对方消息 -->
<view wx:elif="{{item.type === 'message' && item.sender === 'other'}}" class="wechat-message-item wechat-other-message">
<view class="wechat-avatar">
<text class="wechat-avatar-text">{{userName ? userName.charAt(0) : (avatar || '用')}}</text>
</view>
<!-- 图片消息直接显示 -->
<image wx:if="{{item.isImage}}" src="{{item.content}}" class="wechat-message-image wechat-other-image" mode="aspectFit" bindtap="previewImage" data-src="{{item.content}}" />
<!-- 文本消息仍然显示在气泡中 -->
<view wx:elif="{{!item.isImage}}" class="wechat-message-wrapper">
<view class="wechat-message-name">{{userName}}</view>
<view class="wechat-message-bubble wechat-other-bubble">
<text class="wechat-message-text">{{item.content}}</text>
</view>
</view>
</view>
<!-- 我的消息 -->
<view wx:elif="{{item.type === 'message' && item.sender === 'me'}}" class="wechat-message-item wechat-my-message">
<!-- 图片消息直接显示 -->
<image wx:if="{{item.isImage}}" src="{{item.content}}" class="wechat-message-image wechat-my-image" mode="aspectFit" bindtap="previewImage" data-src="{{item.content}}" />
<!-- 文本消息仍然显示在气泡中 -->
<view wx:elif="{{!item.isImage}}" class="wechat-message-wrapper wechat-my-wrapper">
<view class="wechat-message-bubble wechat-my-bubble">
<text class="wechat-message-text wechat-my-text">{{item.content}}</text>
</view>
</view>
<view class="wechat-avatar wechat-my-avatar">
<text class="wechat-avatar-text">我</text>
</view>
</view>
</block>
</view>
</scroll-view>
<!-- 输入区域 - 微信风格 -->
<view class="wechat-input-area">
<view class="wechat-input-toolbar">
<view class="wechat-input-left">
<view class="wechat-emoji-btn" bindtap="showEmoji">😊</view>
<view class="wechat-voice-btn" bindtap="toggleVoiceMode">🔊</view>
</view>
<view class="wechat-input-wrapper">
<input
class="wechat-message-input"
value="{{inputValue}}"
bindinput="onInput"
placeholder="消息"
placeholder-style="color: #999999;"
adjust-position
maxlength="500"
/>
</view>
<view class="wechat-input-right">
<view class="wechat-more-btn" bindtap="showMoreOptions">+</view>
<view class="wechat-send-btn" bindtap="sendMessage" disabled="{{!inputValue.trim()}}">发送</view>
</view>
</view>
</view>
</view>

468
pages/chat-detail/index.wxss

@ -0,0 +1,468 @@
/* 微信聊天界面样式 */
.chat-detail-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #e5e5e5;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 连接状态显示样式 */
.connection-status {
padding: 8rpx 0;
text-align: center;
font-size: 24rpx;
color: #fff;
position: relative;
z-index: 10;
}
.connection-status.connecting {
background-color: #FF9800;
}
.connection-status.connected {
background-color: #4CAF50;
}
.connection-status.error {
background-color: #F44336;
}
.connection-status.disconnected {
background-color: #9E9E9E;
}
.status-text {
display: inline-block;
padding: 0 20rpx;
border-radius: 10rpx;
}
.reconnect-btn {
display: inline-block;
margin-left: 20rpx;
padding: 0 20rpx;
font-size: 24rpx;
line-height: 40rpx;
background-color: rgba(255, 255, 255, 0.3);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 16rpx;
}
.test-mode-switch {
padding: 20rpx;
background-color: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1rpx solid #e8e8e8;
}
.mode-label {
margin-left: 20rpx;
font-size: 28rpx;
color: #666;
}
/* 消息列表区域 - 微信风格 */
.wechat-message-list {
flex: 1;
padding: 20rpx 20rpx 40rpx;
box-sizing: border-box;
overflow-y: auto;
background-color: #e5e5e5;
}
/* 消息容器 */
.wechat-message-container {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 时间显示 */
.wechat-time-display {
display: flex;
justify-content: center;
margin: 20rpx 0;
}
.wechat-time-text {
background-color: rgba(0, 0, 0, 0.1);
color: #fff;
font-size: 24rpx;
padding: 6rpx 20rpx;
border-radius: 12rpx;
}
/* 系统消息 */
.wechat-system-message {
display: flex;
justify-content: center;
margin: 10rpx 0;
}
.wechat-system-content {
background-color: rgba(255, 255, 255, 0.8);
border-radius: 18rpx;
padding: 16rpx 24rpx;
max-width: 80%;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.wechat-warning-title {
color: #ff0000;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.wechat-system-text {
color: #606266;
font-size: 26rpx;
line-height: 36rpx;
}
/* 商品信息卡片 */
.wechat-goods-card {
display: flex;
background-color: #ffffff;
border-radius: 18rpx;
padding: 20rpx;
margin: 10rpx 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
align-self: flex-start;
max-width: 80%;
}
.wechat-goods-avatar {
width: 120rpx;
height: 120rpx;
background-color: #f0f0f0;
border-radius: 8rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.wechat-goods-content {
flex: 1;
min-width: 0;
}
.wechat-goods-title {
font-size: 28rpx;
color: #303133;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 10rpx;
}
.wechat-goods-desc {
font-size: 26rpx;
color: #606266;
display: block;
margin-bottom: 10rpx;
line-height: 36rpx;
}
.wechat-goods-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.wechat-goods-price {
font-size: 32rpx;
color: #e64340;
font-weight: bold;
}
.wechat-goods-type {
font-size: 24rpx;
color: #909399;
background-color: #f0f9ff;
padding: 4rpx 16rpx;
border-radius: 12rpx;
}
/* 消息项 */
.wechat-message-item {
display: flex;
margin: 10rpx 0;
align-items: flex-end;
}
/* 对方消息 */
.wechat-other-message {
flex-direction: row;
}
/* 我的消息 */
.wechat-my-message {
flex-direction: row-reverse;
}
/* 头像 - 圆形 */
.wechat-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
background-color: #9aa5b1;
display: flex;
align-items: center;
justify-content: center;
margin: 0 12rpx;
flex-shrink: 0;
overflow: hidden;
}
.wechat-my-avatar {
background-color: #d8d8d8;
}
.wechat-avatar-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
}
/* 消息包装器 */
.wechat-message-wrapper {
max-width: 75%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6rpx;
}
/* 客服姓名显示 */
.wechat-message-name {
font-size: 24rpx;
color: #909399;
margin-left: 4rpx;
margin-bottom: 2rpx;
}
.wechat-my-wrapper {
align-items: flex-end;
}
/* 消息气泡 - 添加三角形箭头 */
.wechat-message-bubble {
position: relative;
padding: 14rpx 20rpx;
max-width: 100%;
word-wrap: break-word;
word-break: break-all;
}
.wechat-other-bubble {
background-color: #ffffff;
border-radius: 18rpx;
}
.wechat-my-bubble {
background-color: #07c160;
border-radius: 18rpx;
}
/* 对方消息气泡三角形 */
.wechat-other-bubble::before {
content: '';
position: absolute;
left: -10rpx;
bottom: 18rpx;
width: 0;
height: 0;
border-style: solid;
border-width: 10rpx 10rpx 10rpx 0;
border-color: transparent #ffffff transparent transparent;
}
/* 我的消息气泡三角形 */
.wechat-my-bubble::before {
content: '';
position: absolute;
right: -10rpx;
bottom: 18rpx;
width: 0;
height: 0;
border-style: solid;
border-width: 10rpx 0 10rpx 10rpx;
border-color: transparent transparent transparent #07c160;
}
/* 消息文本 */
.wechat-message-text {
font-size: 32rpx;
line-height: 44rpx;
color: #303133;
}
.wechat-my-text {
color: #ffffff;
}
/* 图片消息样式 - 使用微信插件风格 */
.wechat-message-image {
width: 280rpx;
height: 280rpx;
max-width: 75%;
max-height: 500rpx;
border-radius: 16rpx;
margin: 8rpx 0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
background-color: #f5f5f5;
}
/* 对方图片消息样式 - 左侧缩进 */
.wechat-other-image {
margin-left: 16rpx;
align-self: flex-start;
}
/* 我的图片消息样式 - 右侧缩进 */
.wechat-my-image {
margin-right: 16rpx;
align-self: flex-end;
}
/* 消息项调整,确保图片和文本消息对齐 */
.wechat-message-item {
align-items: flex-start;
padding: 4rpx 0;
}
/* 消息时间 */
.wechat-message-time {
font-size: 22rpx;
color: #909399;
padding: 0 4rpx;
}
.wechat-my-time {
text-align: right;
}
/* 输入区域 - 微信风格 */
.wechat-input-area {
background-color: #f5f5f5;
border-top: 1rpx solid #d8d8d8;
padding: 12rpx 20rpx;
box-sizing: border-box;
}
/* 输入工具栏 */
.wechat-input-toolbar {
display: flex;
align-items: center;
gap: 16rpx;
}
/* 底部安全区域适配 */
@media screen and (min-height: 812px) {
.wechat-input-area {
padding-bottom: calc(12rpx + env(safe-area-inset-bottom, 0rpx));
}
}
/* 左侧按钮 */
.wechat-input-left {
display: flex;
align-items: center;
gap: 10rpx;
}
/* 右侧按钮 */
.wechat-input-right {
display: flex;
align-items: center;
gap: 10rpx;
}
/* 表情按钮 */
.wechat-emoji-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
/* 语音按钮 */
.wechat-voice-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
/* 更多按钮 */
.wechat-more-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
color: #606266;
}
/* 发送按钮 */
.wechat-send-btn {
background-color: #07c160;
color: #ffffff;
font-size: 28rpx;
padding: 0 28rpx;
height: 64rpx;
line-height: 64rpx;
border-radius: 32rpx;
text-align: center;
}
/* 发送按钮禁用状态 */
.wechat-send-btn:disabled {
background-color: #c8c8c8;
color: #ffffff;
cursor: not-allowed;
}
/* 发送按钮点击状态 */
.wechat-send-btn:active:not(:disabled) {
background-color: #06b354;
}
/* 输入框包装器 */
.wechat-input-wrapper {
flex: 1;
background-color: #ffffff;
border: 1rpx solid #d8d8d8;
border-radius: 32rpx;
padding: 0 20rpx;
min-height: 64rpx;
max-height: 180rpx;
display: flex;
align-items: center;
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
/* 消息输入框 */
.wechat-message-input {
flex: 1;
font-size: 32rpx;
color: #303133;
min-height: 44rpx;
max-height: 160rpx;
line-height: 44rpx;
padding: 0;
margin: 10rpx 0;
}

474
pages/chat/index.js

@ -0,0 +1,474 @@
// pages/chat/index.js
import socketManager from '../../utils/websocket.js';
Page({
/**
* 页面的初始数据
*/
data: {
messages: [], // 初始为空数组,后续通过数据获取和WebSocket更新
activeTab: 'all',
webSocketUrl: '' // WebSocket服务器地址
},
// WebSocket消息处理函数
handleWebSocketMessage: function(message) {
console.log('聊天列表页面接收到消息:', message);
// 判断是否为新消息 - 支持'new_message'类型(服务器实际发送的类型)
if (message.type === 'new_message') {
const newMessage = message.payload;
this.updateMessageList(newMessage);
} else if (message.type === 'chat_message') {
// 保留原有的'chat_message'类型支持,兼容不同的消息格式
const newMessage = message.data;
this.updateMessageList(newMessage);
}
},
// 更新消息列表
updateMessageList: function(newMessage) {
console.log('更新消息列表:', newMessage);
const messages = [...this.data.messages];
// 确定消息发送者ID - 处理服务器返回的数据格式
const senderId = newMessage.senderId;
const existingIndex = messages.findIndex(item => item.id === senderId);
// 格式化消息时间 - 服务器使用createdAt字段
const now = new Date();
const messageDate = new Date(newMessage.createdAt || newMessage.timestamp || Date.now());
let displayTime = '';
if (messageDate.toDateString() === now.toDateString()) {
// 今天的消息只显示时间
displayTime = messageDate.getHours().toString().padStart(2, '0') + ':' +
messageDate.getMinutes().toString().padStart(2, '0');
} else if (messageDate.toDateString() === new Date(now.getTime() - 86400000).toDateString()) {
// 昨天的消息
displayTime = '昨天';
} else {
// 其他日期显示月日
displayTime = (messageDate.getMonth() + 1) + '月' + messageDate.getDate() + '日';
}
// 获取当前用户ID,确定消息方向
const app = getApp();
const currentUserId = app.globalData.userInfo?.userId || wx.getStorageSync('userId') || 'unknown';
// 确定显示的用户ID(如果是自己发送的消息,显示接收者ID)
const displayUserId = senderId === currentUserId ? newMessage.receiverId : senderId;
if (existingIndex >= 0) {
// 存在该用户的消息,更新内容和时间
messages[existingIndex] = {
...messages[existingIndex],
content: newMessage.content || '',
time: displayTime,
isRead: false
};
// 将更新的消息移到列表顶部
const [updatedMessage] = messages.splice(existingIndex, 1);
messages.unshift(updatedMessage);
} else {
// 新用户消息,添加到列表顶部
// 这里暂时使用ID作为用户名,实际应用中应该从用户信息中获取
const displayName = `用户${displayUserId}`;
messages.unshift({
id: displayUserId,
name: displayName,
avatar: displayName.charAt(0),
content: newMessage.content || '',
time: displayTime,
isRead: false
});
}
// 使用setData更新视图
this.setData({ messages });
// 触发消息提示振动(可选)
wx.vibrateShort();
// 更新TabBar未读消息数(如果需要)
this.updateTabBarBadge();
},
// 更新TabBar未读消息提示 - 使用自定义TabBar兼容方式
updateTabBarBadge: function() {
console.log('更新TabBar未读提示,当前消息数:', this.data.messages.length);
// 检查是否有未读消息
const hasUnread = this.data.messages.some(msg => !msg.isRead);
console.log('是否有未读消息:', hasUnread);
// 对于自定义TabBar,使用全局状态来管理未读标记
const app = getApp();
if (app && app.globalData) {
app.globalData.tabBarBadge = {
chat: hasUnread ? ' ' : ''
};
console.log('已更新全局TabBar未读标记状态:', hasUnread ? '显示' : '隐藏');
// 尝试通过getTabBar方法通知自定义TabBar更新
try {
const tabBar = this.getTabBar();
if (tabBar) {
tabBar.setData({
selected: 'buyer', // 假设聊天页是buyer tab
badge: hasUnread ? ' ' : ''
});
}
} catch (e) {
console.log('TabBar更新失败,将在下一次页面显示时自动更新');
}
}
},
// 清理所有未读消息状态
clearAllUnreadStatus: function() {
console.log('清理所有未读消息状态');
try {
// 1. 更新本地消息列表中的未读状态
const updatedMessages = this.data.messages.map(msg => ({
...msg,
isRead: true
}));
this.setData({
messages: updatedMessages
});
// 2. 对于自定义TabBar,使用全局状态来管理未读标记
const app = getApp();
if (app && app.globalData) {
app.globalData.tabBarBadge = {
chat: ''
};
console.log('已清理全局TabBar未读标记');
// 尝试通过getTabBar方法通知自定义TabBar更新
try {
const tabBar = this.getTabBar();
if (tabBar) {
tabBar.setData({
selected: 'buyer', // 假设聊天页是buyer tab
badge: ''
});
}
} catch (e) {
console.log('TabBar更新失败,将在下一次页面显示时自动更新');
}
}
// 3. 显示成功提示
wx.showToast({
title: '已清除所有未读提示',
icon: 'success',
duration: 2000
});
} catch (error) {
console.error('清理未读状态失败:', error);
wx.showToast({
title: '清理失败',
icon: 'none',
duration: 2000
});
}
},
// 初始化WebSocket连接
initWebSocket: function() {
try {
const app = getApp();
// 使用正确的WebSocket服务器地址 - 开发环境通常是ws://localhost:3003
// 动态构建WebSocket URL,基于全局配置或环境
let wsProtocol = 'ws://';
let wsHost = app.globalData.webSocketUrl || 'localhost:3003';
let wsUrl;
// 如果wsHost已经包含协议,直接使用
if (wsHost.startsWith('ws://') || wsHost.startsWith('wss://')) {
wsUrl = wsHost;
} else {
// 否则添加协议前缀
wsUrl = `${wsProtocol}${wsHost}`;
}
this.setData({ webSocketUrl: wsUrl });
console.log('WebSocket连接初始化,使用地址:', wsUrl);
// 连接WebSocket
socketManager.connect(wsUrl);
// 添加消息监听
socketManager.on('message', this.handleWebSocketMessage.bind(this));
// 添加状态监听,以便调试
socketManager.on('status', (status) => {
console.log('WebSocket状态更新:', status);
});
console.log('聊天列表页面WebSocket已初始化');
} catch (error) {
console.error('初始化WebSocket失败:', error);
wx.showToast({
title: 'WebSocket初始化失败',
icon: 'none'
});
}
},
// 清理WebSocket连接
cleanupWebSocket: function() {
socketManager.off('message', this.handleWebSocketMessage);
},
// 加载聊天列表数据
loadChatList: function() {
wx.showLoading({ title: '加载中' });
try {
// 从服务器获取真实的聊天列表数据
const app = getApp();
const token = app.globalData.token || wx.getStorageSync('token');
// 使用正确的API配置,兼容开发环境
const baseUrl = app.globalData.baseUrl || 'http://localhost:3003';
const currentUserId = app.globalData.userInfo?.userId || wx.getStorageSync('userId') || 'unknown';
console.log('使用API地址:', baseUrl);
console.log('当前用户ID:', currentUserId);
// 使用正确的API端点 - /api/conversations/user/:userId
wx.request({
url: `${baseUrl}/api/conversations/user/${currentUserId}`,
method: 'GET',
header: {
'Authorization': token ? `Bearer ${token}` : '',
'content-type': 'application/json'
},
success: (res) => {
console.log('获取聊天列表成功:', res.data);
// 处理不同的API响应格式
let chatData = [];
if (res.data.code === 0 && res.data.data) {
chatData = res.data.data;
} else if (Array.isArray(res.data)) {
// 如果直接返回数组
chatData = res.data;
} else if (res.data) {
// 如果返回的是对象但不是标准格式,尝试直接使用
chatData = [res.data];
}
if (chatData.length > 0) {
// 格式化聊天列表数据
const formattedMessages = chatData.map(item => {
// 格式化时间
const now = new Date();
const messageDate = new Date(item.lastMessageTime || item.createdAt || Date.now());
let displayTime = '';
if (messageDate.toDateString() === now.toDateString()) {
// 今天的消息只显示时间
displayTime = messageDate.getHours().toString().padStart(2, '0') + ':' +
messageDate.getMinutes().toString().padStart(2, '0');
} else if (messageDate.toDateString() === new Date(now.getTime() - 86400000).toDateString()) {
// 昨天的消息
displayTime = '昨天';
} else {
// 其他日期显示月日
displayTime = (messageDate.getMonth() + 1) + '月' + messageDate.getDate() + '日';
}
// 获取当前用户ID,确定显示哪个用户
const displayUserId = item.userId === currentUserId ? item.managerId : item.userId || item.id;
const displayName = `用户${displayUserId}`;
return {
id: displayUserId,
name: item.userName || item.name || displayName,
avatar: item.avatar || (item.userName || displayName).charAt(0) || '用',
content: item.lastMessage || item.content || '',
time: displayTime,
isRead: item.isRead || false,
unreadCount: item.unreadCount || 0
};
});
this.setData({ messages: formattedMessages });
} else {
console.log('暂无聊天消息');
this.setData({ messages: [] });
}
},
fail: (err) => {
console.error('网络请求失败:', err);
wx.showToast({
title: '网络请求失败,使用本地数据',
icon: 'none',
duration: 3000
});
// 失败时使用模拟数据,确保页面能够正常显示
this.setData({
messages: [
{
id: '1',
name: '系统消息',
avatar: '系',
content: '欢迎使用聊天功能',
time: '刚刚',
isRead: false
}
]
});
},
complete: () => {
wx.hideLoading();
}
});
} catch (error) {
console.error('加载聊天列表异常:', error);
wx.hideLoading();
wx.showToast({ title: '加载异常', icon: 'none' });
}
},
// 返回上一页
onBack: function() {
wx.navigateBack({
delta: 1
});
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadChatList();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 设置导航栏右侧按钮的点击事件
wx.showNavigationBarLoading();
},
// 导航栏左侧按钮点击事件
onNavigationBarButtonTap(e) {
if (e.type === 'left') {
this.onBack();
} else if (e.type === 'right') {
// 处理管理按钮点击
wx.showToast({
title: '管理功能待开发',
icon: 'none'
});
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时初始化WebSocket连接
this.initWebSocket();
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
// 页面隐藏时清理WebSocket连接
this.cleanupWebSocket();
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
// 页面卸载时清理WebSocket连接
this.cleanupWebSocket();
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
// 页面分享配置
return {
title: '聊天列表',
path: '/pages/chat/index'
}
},
// 跳转到对话详情页面
navigateToChatDetail: function(e) {
const userId = e.currentTarget.dataset.userId;
const userName = e.currentTarget.dataset.userName;
// 将该聊天标记为已读
const messages = [...this.data.messages];
const messageIndex = messages.findIndex(item => item.id === userId);
if (messageIndex >= 0) {
messages[messageIndex].isRead = true;
messages[messageIndex].unreadCount = 0;
this.setData({ messages });
// 更新TabBar未读消息数
this.updateTabBarBadge();
// 通知服务器已读状态(可选)
this.markAsRead(userId);
}
wx.navigateTo({
url: `/pages/chat-detail/index?userId=${userId}&userName=${encodeURIComponent(userName)}`
});
},
// 通知服务器消息已读
markAsRead: function(userId) {
const app = getApp();
const token = app.globalData.token || wx.getStorageSync('token');
wx.request({
url: `${app.globalData.baseUrl || 'https://your-server.com'}/api/chat/read`,
method: 'POST',
header: {
'Authorization': `Bearer ${token}`,
'content-type': 'application/json'
},
data: {
userId: userId
},
success: (res) => {
console.log('标记消息已读成功:', res.data);
},
fail: (err) => {
console.error('标记消息已读失败:', err);
}
});
}
})

8
pages/chat/index.json

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "消息",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationBarLeftButtonText": "返回",
"navigationBarRightButtonText": "管理",
"usingComponents": {}
}

43
pages/chat/index.wxml

@ -0,0 +1,43 @@
<view class="chat-container">
<!-- 页面顶部导航已在index.json中配置 -->
<!-- 消息类型切换 -->
<view class="message-tabs">
<view class="tab-item active">全部</view>
<view class="tab-item">未读</view>
</view>
<!-- 清除未读提示 -->
<view class="clear-unread">
<text class="clear-btn" bindtap="clearAllUnreadStatus">清除未读</text>
</view>
<!-- 消息列表 -->
<view class="message-list">
<!-- 提示消息 -->
<view class="message-tips">
以下为3天前的消息,提示将弱化
</view>
<!-- 动态消息列表 -->
<block wx:if="{{messages.length > 0}}">
<view wx:for="{{messages}}" wx:key="id" class="message-item" bindtap="navigateToChatDetail" data-user-id="{{item.id}}" data-user-name="{{item.name}}">
<view class="message-avatar">
<text class="avatar-icon">{{item.avatar}}</text>
<view wx:if="{{!item.isRead}}" class="unread-dot"></view>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-name">{{item.name}}</text>
<text class="message-time">{{item.time}}</text>
</view>
<text class="message-text {{!item.isRead ? 'unread' : ''}}">{{item.content}}</text>
</view>
</view>
</block>
<!-- 空状态提示 -->
<view wx:else class="empty-state">
<text class="empty-text">暂无消息</text>
</view>
</view>
</view>

182
pages/chat/index.wxss

@ -0,0 +1,182 @@
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
padding-top: 0;
}
/* 消息类型切换 */
.message-tabs {
display: flex;
background-color: #ffffff;
border-bottom: 1rpx solid #eeeeee;
width: 100%;
box-sizing: border-box;
}
.tab-item {
flex: 1;
text-align: center;
padding: 28rpx 0;
font-size: 32rpx;
color: #666666;
position: relative;
}
.tab-item.active {
color: #07c160;
font-weight: 500;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 40%;
width: 20%;
height: 4rpx;
background-color: #07c160;
}
/* 清除未读提示 */
.clear-unread {
display: flex;
justify-content: flex-end;
padding: 20rpx 30rpx;
background-color: #ffffff;
box-sizing: border-box;
width: 100%;
}
.clear-btn {
font-size: 28rpx;
color: #1677ff;
}
/* 消息列表 */
.message-list {
flex: 1;
padding-bottom: 30rpx;
overflow-y: auto;
}
/* 提示消息 */
.message-tips {
text-align: center;
padding: 20rpx 0;
margin-bottom: 20rpx;
color: #999999;
font-size: 28rpx;
}
/* 消息项 */
.message-item {
display: flex;
padding: 24rpx 30rpx;
background-color: #ffffff;
position: relative;
}
/* 添加底部边框线,模仿微信分隔线 */
.message-item::after {
content: '';
position: absolute;
left: 150rpx;
right: 0;
bottom: 0;
height: 1rpx;
background-color: #f0f0f0;
}
/* 头像 */
.message-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
background-color: #07c160;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
position: relative;
overflow: hidden;
}
/* 未读消息红点 */
.unread-dot {
position: absolute;
top: -6rpx;
right: -6rpx;
width: 28rpx;
height: 28rpx;
border-radius: 14rpx;
background-color: #ff3b30;
border: 2rpx solid #ffffff;
}
.avatar-icon {
font-size: 40rpx;
color: #ffffff;
font-weight: 500;
}
/* 消息内容 */
.message-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 6rpx 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.message-name {
font-size: 32rpx;
color: #000000;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-time {
font-size: 24rpx;
color: #999999;
margin-left: 12rpx;
}
.message-text {
font-size: 28rpx;
color: #999999;
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 未读消息文本样式 */
.message-text.unread {
color: #000000;
font-weight: 500;
}
/* 空状态样式 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx 0;
color: #999999;
}
.empty-text {
font-size: 32rpx;
color: #999999;
}

165
pages/customer-service/detail/index.js

@ -0,0 +1,165 @@
// pages/customer-service/detail/index.js
Page({
data: {
customerData: null,
customerServices: [
{
id: 1,
managerId: 'PM001',
managercompany: '鸡蛋贸易有限公司',
managerdepartment: '采购部',
organization: '鸡蛋采购组',
projectName: '高级采购经理',
name: '张三',
alias: '张经理',
phoneNumber: '13800138001',
avatarUrl: '',
score: 999,
isOnline: true,
responsibleArea: '华北区鸡蛋采购',
experience: '1-3年',
serviceCount: 200,
purchaseCount: 15000,
profitIncreaseRate: 15,
profitFarmCount: 120,
skills: ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格']
},
{
id: 2,
managerId: 'PM002',
managercompany: '鸡蛋贸易有限公司',
managerdepartment: '采购部',
organization: '全国采购组',
projectName: '采购经理',
name: '李四',
alias: '李经理',
phoneNumber: '13900139002',
avatarUrl: '',
score: 998,
isOnline: true,
responsibleArea: '全国鸡蛋采购',
experience: '2-3年',
serviceCount: 200,
purchaseCount: 20000,
profitIncreaseRate: 18,
profitFarmCount: 150,
skills: ['精准把控市场价格', '渠道拓展', '供应商维护']
},
{
id: 3,
managerId: 'PM003',
managercompany: '鸡蛋贸易有限公司',
managerdepartment: '采购部',
organization: '华东采购组',
projectName: '采购专员',
name: '王五',
alias: '王专员',
phoneNumber: '13700137003',
avatarUrl: '',
score: 997,
isOnline: false,
responsibleArea: '华东区鸡蛋采购',
experience: '1-2年',
serviceCount: 150,
purchaseCount: 12000,
profitIncreaseRate: 12,
profitFarmCount: 80,
skills: ['质量把控', '供应商维护']
},
{
id: 4,
managerId: 'PM004',
managercompany: '鸡蛋贸易有限公司',
managerdepartment: '采购部',
organization: '华南采购组',
projectName: '高级采购经理',
name: '赵六',
alias: '赵经理',
phoneNumber: '13600136004',
avatarUrl: '',
score: 996,
isOnline: true,
responsibleArea: '华南区鸡蛋采购',
experience: '3-5年',
serviceCount: 250,
purchaseCount: 25000,
profitIncreaseRate: 20,
profitFarmCount: 180,
skills: ['精准把控市场价格', '渠道拓展', '质量把控', '供应商维护']
}
]
},
onLoad: function (options) {
// 获取传递过来的客服ID
const { id } = options;
// 根据ID查找客服数据
const customerData = this.data.customerServices.find(item => item.id === parseInt(id));
if (customerData) {
this.setData({
customerData: customerData
});
// 设置导航栏标题
wx.setNavigationBarTitle({
title: `${customerData.alias} - 客服详情`,
});
} else {
// 如果找不到对应ID的客服,显示错误提示
wx.showToast({
title: '未找到客服信息',
icon: 'none'
});
}
},
onShow() {
// 更新自定义tabBar状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: -1 // 不选中任何tab
});
}
},
// 返回上一页
onBack: function () {
wx.navigateBack();
},
// 在线沟通
onChat: function () {
const { customerData } = this.data;
if (customerData) {
wx.navigateTo({
url: `/pages/chat/index?id=${customerData.id}&name=${customerData.alias}&phone=${customerData.phoneNumber}`
});
}
},
// 拨打电话
onCall: function () {
const { customerData } = this.data;
if (customerData && customerData.phoneNumber) {
wx.makePhoneCall({
phoneNumber: customerData.phoneNumber,
success: function () {
console.log('拨打电话成功');
},
fail: function () {
console.log('拨打电话失败');
}
});
}
},
// 分享功能
onShareAppMessage: function () {
const { customerData } = this.data;
return {
title: `${customerData?.alias || '优秀客服'} - 鸡蛋贸易平台`,
path: `/pages/customer-service/detail/index?id=${customerData?.id}`,
imageUrl: ''
};
}
});

7
pages/customer-service/detail/index.json

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "客服详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8f8f8",
"usingComponents": {}
}

93
pages/customer-service/detail/index.wxml

@ -0,0 +1,93 @@
<!-- pages/customer-service/detail/index.wxml -->
<view class="container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" bindtap="onBack">
<text class="back-icon">返回</text>
</view>
<view class="nav-title">客服详情</view>
<view class="nav-right">
<text class="share-icon">📤</text>
</view>
</view>
<!-- 客服基本信息 -->
<view class="info-section">
<view class="header-info">
<view class="avatar-container">
<image class="avatar" src="{{customerData.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill" />
<view wx:if="{{customerData.isOnline}}" class="online-indicator-large">在线</view>
<view wx:else class="offline-indicator-large">离线</view>
</view>
<view class="header-details">
<view class="name-score-row">
<text class="name-large">{{customerData.alias}}</text>
<text class="score-large">{{customerData.score}} 鸡蛋分</text>
</view>
<text class="position">{{customerData.projectName}}</text>
<text class="company-large">{{customerData.managercompany}}</text>
</view>
</view>
</view>
<!-- 核心信息卡片 -->
<view class="core-info-section">
<view class="info-card">
<view class="info-item">
<text class="info-label">负责区域</text>
<text class="info-value">{{customerData.responsibleArea}}</text>
</view>
<view class="info-item">
<text class="info-label">联系电话</text>
<text class="info-value phone-number" bindtap="onCall">{{customerData.phoneNumber}}</text>
</view>
<view class="info-item">
<text class="info-label">工作经验</text>
<text class="info-value">服务平台{{customerData.experience}}</text>
</view>
<view class="info-item">
<text class="info-label">服务规模</text>
<text class="info-value">服务{{customerData.serviceCount}}家鸡场</text>
</view>
</view>
</view>
<!-- 专业技能标签 -->
<view class="skills-section">
<view class="section-title">专业技能</view>
<view class="skills-container">
<view wx:for="{{customerData.skills}}" wx:key="index" class="skill-tag">
{{item}}
</view>
</view>
</view>
<!-- 业绩数据 -->
<view class="performance-section">
<view class="section-title">业绩数据</view>
<view class="performance-cards">
<view class="performance-card">
<view class="performance-number">{{customerData.purchaseCount}}</view>
<view class="performance-label">累计采购鸡蛋(件)</view>
</view>
<view class="performance-card">
<view class="performance-number">{{customerData.profitFarmCount}}</view>
<view class="performance-label">累计服务鸡场(家)</view>
</view>
<view class="performance-card">
<view class="performance-number profit-rate">{{customerData.profitIncreaseRate}}%</view>
<view class="performance-label">平均盈利增长</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-button chat-button" bindtap="onChat">
<text class="button-text">💬 在线沟通</text>
</view>
<view class="action-button call-button" bindtap="onCall">
<text class="button-text">📞 电话联系</text>
</view>
</view>
</view>

317
pages/customer-service/detail/index.wxss

@ -0,0 +1,317 @@
/* pages/customer-service/detail/index.wxss */
.container {
padding-bottom: 100rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 44rpx 30rpx 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
width: 100%;
box-sizing: border-box;
}
.nav-left {
width: 80rpx;
display: flex;
justify-content: flex-start;
}
.back-icon {
font-size: 32rpx;
color: #333;
font-weight: normal;
}
.nav-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
.nav-right {
display: flex;
align-items: center;
gap: 30rpx;
}
.share-icon {
font-size: 32rpx;
color: #333;
}
/* 客服基本信息 */
.info-section {
background-color: #fff;
padding: 120rpx 30rpx 30rpx;
margin-bottom: 20rpx;
}
.header-info {
display: flex;
align-items: center;
}
.avatar-container {
width: 160rpx;
height: 160rpx;
margin-right: 30rpx;
position: relative;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #f0f0f0;
border: 4rpx solid #f0f0f0;
}
.online-indicator-large {
position: absolute;
bottom: 0;
right: 0;
background-color: #52c41a;
color: white;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
border: 4rpx solid white;
}
.offline-indicator-large {
position: absolute;
bottom: 0;
right: 0;
background-color: #999;
color: white;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
border: 4rpx solid white;
}
.header-details {
flex: 1;
}
.name-score-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.name-large {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.score-large {
font-size: 28rpx;
color: #fff;
background: linear-gradient(135deg, #ffb800, #ff7700);
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.position {
font-size: 32rpx;
color: #666;
margin-bottom: 8rpx;
}
.company-large {
font-size: 28rpx;
color: #999;
}
/* 核心信息卡片 */
.core-info-section {
padding: 0 30rpx;
margin-bottom: 20rpx;
}
.info-card {
background-color: #fff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 30rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
color: #333;
text-align: right;
flex: 1;
margin-left: 30rpx;
}
.phone-number {
color: #1890ff;
}
/* 专业技能标签 */
.skills-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.skills-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding-bottom: 10rpx;
}
.skill-tag {
background-color: #f0f9ff;
color: #1890ff;
font-size: 26rpx;
padding: 12rpx 24rpx;
border-radius: 20rpx;
border: 1rpx solid #bae7ff;
transition: all 0.2s ease;
}
.skill-tag:active {
background-color: #bae7ff;
transform: scale(0.98);
}
/* 业绩数据 */
.performance-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 120rpx;
}
.performance-cards {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.performance-card {
flex: 1;
background-color: #f9f9f9;
padding: 24rpx;
border-radius: 16rpx;
text-align: center;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.performance-card:active {
transform: translateY(2rpx);
background-color: #f0f0f0;
}
.performance-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 12rpx;
}
.performance-number.profit-rate {
color: #52c41a;
}
.performance-label {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background-color: #fff;
padding: 20rpx 30rpx;
border-top: 1rpx solid #f0f0f0;
gap: 20rpx;
}
.action-button {
flex: 1;
padding: 24rpx;
border-radius: 40rpx;
text-align: center;
font-size: 32rpx;
font-weight: 500;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.action-button:active {
transform: scale(0.98);
opacity: 0.9;
}
.chat-button:active {
background-color: #096dd9;
}
.call-button:active {
background-color: #389e0d;
}
.chat-button {
background-color: #1890ff;
color: white;
}
.call-button {
background-color: #52c41a;
color: white;
}
.button-text {
font-size: 30rpx;
}

362
pages/customer-service/index.js

@ -0,0 +1,362 @@
// pages/customer-service/index.js
Page({
// 客服数据,将从后端动态获取
data: {
customerServices: [],
filteredServices: [],
searchKeyword: '',
selectedArea: '全部',
totalCount: 0,
onlineCount: 0
},
// 获取客服列表的方法
async fetchCustomerServices() {
try {
console.log('开始请求客服列表...');
// 导入API工具并使用正确的请求方法
const api = require('../../utils/api');
const res = await new Promise((resolve, reject) => {
wx.request({
url: 'http://localhost:3003/api/managers',
method: 'GET',
timeout: 15000,
header: {
'content-type': 'application/json'
},
success: resolve,
fail: (error) => {
console.error('网络请求失败:', error);
reject(error);
}
});
});
console.log('API响应状态码:', res?.statusCode);
console.log('API响应数据:', res?.data ? JSON.stringify(res.data) : 'undefined');
// 更宽松的响应检查,确保能处理各种有效的响应格式
if (res && res.statusCode === 200 && res.data) {
// 无论success字段是否存在,只要有data字段就尝试处理
const dataSource = res.data.data || res.data;
if (Array.isArray(dataSource)) {
const processedData = dataSource.map(item => ({
id: item.id || `id_${Date.now()}_${Math.random()}`, // 确保有id
managerId: item.managerId || '',
managercompany: item.managercompany || '',
managerdepartment: item.managerdepartment || '',
organization: item.organization || '',
projectName: item.projectName || '',
name: item.name || '未知',
alias: item.alias || item.name || '未知',
phoneNumber: item.phoneNumber || '',
avatarUrl: item.avatar || item.avatarUrl || '', // 兼容avatar和avatarUrl
score: Math.floor(Math.random() * 20) + 980, // 随机生成分数
isOnline: !!item.online, // 转换为布尔值
responsibleArea: `${this.getRandomArea()}鸡蛋采购`,
experience: this.getRandomExperience(),
serviceCount: this.getRandomNumber(100, 300),
purchaseCount: this.getRandomNumber(10000, 30000),
profitIncreaseRate: this.getRandomNumber(10, 25),
profitFarmCount: this.getRandomNumber(50, 200),
skills: this.getRandomSkills()
}));
console.log('处理后的数据数量:', processedData.length);
return processedData;
} else {
console.error('响应数据格式错误,不是预期的数组格式:', dataSource);
return [];
}
} else {
console.error('获取客服列表失败,状态码:', res?.statusCode, '响应数据:', res?.data);
return [];
}
} catch (error) {
console.error('请求客服列表出错:', error);
// 网络错误时显示提示
wx.showToast({
title: '网络请求失败,请检查网络连接',
icon: 'none'
});
return [];
}
},
// 辅助函数:生成随机区域
getRandomArea() {
const areas = ['华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'];
return areas[Math.floor(Math.random() * areas.length)];
},
// 辅助函数:生成随机工作经验
getRandomExperience() {
const experiences = ['1-2年', '1-3年', '2-3年', '3-5年', '5年以上'];
return experiences[Math.floor(Math.random() * experiences.length)];
},
// 辅助函数:生成随机数字
getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
// 辅助函数:生成随机技能
getRandomSkills() {
const allSkills = ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格', '谈判技巧', '库存管理'];
const skillCount = Math.floor(Math.random() * 3) + 2; // 2-4个技能
const selectedSkills = [];
while (selectedSkills.length < skillCount) {
const skill = allSkills[Math.floor(Math.random() * allSkills.length)];
if (!selectedSkills.includes(skill)) {
selectedSkills.push(skill);
}
}
return selectedSkills;
},
// 设置WebSocket监听客服状态变化
setupWebSocketListener() {
const ws = getApp().globalData.webSocketManager;
const app = getApp();
const userInfo = app.globalData.userInfo || {};
const isManager = userInfo.userType === 'manager' || userInfo.type === 'manager';
if (ws) {
// 监听客服状态更新消息
ws.on('customerServiceStatusUpdate', (data) => {
console.log('收到客服状态更新:', data);
// 更新对应客服的在线状态
const customerServiceList = this.data.customerServices;
const updatedList = customerServiceList.map(item => {
if (item.id === data.id || item.managerId === data.managerId) {
return { ...item, isOnline: data.isOnline };
}
return item;
});
const onlineCount = updatedList.filter(item => item.isOnline).length;
this.setData({
customerServices: updatedList,
onlineCount: onlineCount
});
// 重新应用筛选
this.filterServices();
});
// 如果当前用户是客服,立即进行认证
if (isManager && userInfo.userId) {
console.log('客服用户登录,进行WebSocket认证:', userInfo.userId);
// 使用userId作为managerId进行认证,确保与服务器端onlineManagers中的键匹配
ws.authenticate();
}
}
},
// 定期刷新客服列表(每30秒)
startPeriodicRefresh() {
this.refreshTimer = setInterval(() => {
console.log('定期刷新客服列表...');
this.loadCustomerServices();
}, 30000);
},
// 停止定期刷新
stopPeriodicRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
},
// 加载客服列表
async loadCustomerServices() {
console.log('开始加载客服列表...');
// 确保在开始时调用hideLoading,防止重复调用showLoading
try {
wx.hideLoading();
} catch (e) {
console.log('没有正在显示的loading');
}
wx.showLoading({ title: '加载中...' });
try {
const services = await this.fetchCustomerServices();
console.log('获取到的客服数量:', services.length);
// 计算在线数量
const onlineCount = services.filter(item => item.isOnline).length;
console.log('在线客服数量:', onlineCount);
// 更新数据
this.setData({
customerServices: services,
totalCount: services.length,
onlineCount: onlineCount
});
console.log('数据更新成功');
// 应用当前的筛选条件
this.filterServices();
console.log('筛选条件应用完成');
// 如果没有数据,显示提示(确保在hideLoading后显示)
if (services.length === 0) {
setTimeout(() => {
wx.showToast({
title: '暂无客服数据',
icon: 'none',
duration: 2000
});
}, 100);
}
} catch (error) {
console.error('加载客服列表失败:', error);
// 确保在hideLoading后显示错误提示
setTimeout(() => {
wx.showToast({
title: '加载失败,请重试',
icon: 'none',
duration: 2000
});
}, 100);
} finally {
wx.hideLoading();
}
},
onLoad: function () {
// 初始化WebSocket连接
const app = getApp();
if (!app.globalData.webSocketManager) {
// 如果WebSocket管理器还没初始化,从utils导入
const WebSocketManager = require('../../utils/websocket').default;
app.globalData.webSocketManager = WebSocketManager;
// 尝试连接WebSocket
WebSocketManager.connect('ws://localhost:3003');
}
// 设置WebSocket监听
this.setupWebSocketListener();
// 加载客服列表
this.loadCustomerServices();
// 启动定期刷新
this.startPeriodicRefresh();
},
onShow() {
// 更新自定义tabBar状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: -1 // 不选中任何tab
});
}
// 当页面显示时重新加载数据,确保数据最新
this.loadCustomerServices();
},
onUnload: function() {
// 停止定期刷新
this.stopPeriodicRefresh();
// 清理WebSocket事件监听
const ws = getApp().globalData.webSocketManager;
if (ws) {
ws.off('customerServiceStatusUpdate');
}
},
onSearch: function (e) {
const keyword = e.detail.value;
this.setData({
searchKeyword: keyword
});
this.filterServices();
},
onAreaFilter: function () {
// 区域筛选弹窗 - 鸡蛋采购区域
wx.showActionSheet({
itemList: ['全部', '华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'],
success: res => {
const areas = ['全部', '华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'];
const selectedArea = areas[res.tapIndex];
this.setData({
selectedArea: selectedArea
});
this.filterServices();
}
});
},
filterServices: function () {
const { customerServices, searchKeyword, selectedArea } = this.data;
let filtered = customerServices;
// 关键词搜索
if (searchKeyword) {
const keyword = searchKeyword.toLowerCase();
filtered = filtered.filter(item => {
return item.alias?.toLowerCase().includes(keyword) ||
item.name.toLowerCase().includes(keyword) ||
item.phoneNumber?.includes(keyword) ||
item.managercompany?.toLowerCase().includes(keyword);
});
}
// 区域筛选
if (selectedArea && selectedArea !== '全部') {
filtered = filtered.filter(item => {
return item.responsibleArea?.includes(selectedArea);
});
}
this.setData({
filteredServices: filtered
});
},
onChat: function (e) {
const id = e.currentTarget.dataset.id;
const service = this.data.customerServices.find(item => item.id === id);
// 确保使用managerId作为聊天对象的唯一标识符
const chatUserId = service?.managerId || id;
wx.navigateTo({
url: `/pages/chat-detail/index?userId=${chatUserId}&userName=${encodeURIComponent(service?.alias || '')}&phone=${service?.phoneNumber || ''}&isManager=true`
});
console.log('跳转到聊天页面:', { chatUserId, userName: service?.alias });
},
onCall: function (e) {
const phone = e.currentTarget.dataset.phone;
wx.makePhoneCall({
phoneNumber: phone,
success: function () {
console.log('拨打电话成功');
},
fail: function () {
console.log('拨打电话失败');
}
});
},
// 查看客服详情
onViewDetail: function (e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/customer-service/detail?id=${id}`
});
},
onBack: function () {
wx.navigateBack();
}
});

5
pages/customer-service/index.json

@ -0,0 +1,5 @@
{
"navigationBarBackgroundColor": "#f8f8f8",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

89
pages/customer-service/index.wxml

@ -0,0 +1,89 @@
<!-- pages/customer-service/index.wxml -->
<view class="container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" bindtap="onBack">
<text class="back-icon">返回</text>
</view>
<view class="nav-title">客服列表</view>
<view class="nav-right">
<text class="settings-icon">⚙️</text>
</view>
</view>
<!-- 搜索区域 -->
<view class="search-container">
<view class="search-bar">
<text class="search-icon-small">🔍</text>
<input class="search-input" placeholder="客服人称或手机号" bindinput="onSearch" value="{{searchKeyword}}" />
</view>
<view class="filter-area">
<view class="area-picker" bindtap="onAreaFilter">
<text>区域</text>
<text class="picker-arrow">▼</text>
</view>
</view>
</view>
<!-- 客服列表 -->
<view class="broker-list">
<block wx:if="{{filteredServices.length > 0}}">
<view class="broker-item" wx:for="{{filteredServices}}" wx:key="id" bindtap="onViewDetail" data-id="{{item.id}}">
<view class="broker-info">
<view class="avatar-container">
<image class="avatar" src="{{item.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill" />
<view wx:if="{{item.isOnline}}" class="online-indicator">在线</view>
<view wx:else class="offline-indicator">离线</view>
</view>
<view class="broker-details">
<view class="name-row">
<text class="name">{{item.alias}}</text>
<text class="score-text">{{item.score}} 鸡蛋分</text>
<text wx:if="{{item.isOnline}}" class="online-status">(在线)</text>
</view>
<text class="company">{{item.managercompany || '暂无公司信息'}}</text>
<text class="department">{{item.managerdepartment}} · {{item.projectName}}</text>
<text class="area">负责区域:{{item.responsibleArea}}</text>
<text class="experience">服务平台{{item.experience}} 服务{{item.serviceCount}}家鸡场</text>
<!-- 业绩数据统计 -->
<view class="performance-stats">
<view class="stat-item">
<text class="stat-value">{{item.purchaseCount}}</text>
<text class="stat-label">累计采购(件)</text>
</view>
<view class="stat-divider">|</view>
<view class="stat-item">
<text class="stat-value profit-rate">{{item.profitFarmCount}}</text>
<text class="stat-label">服务盈利鸡场(家)</text>
</view>
<view class="stat-divider">|</view>
<view class="stat-item">
<text class="stat-value profit-rate">{{item.profitIncreaseRate}}%</text>
<text class="stat-label">平均盈利增长</text>
</view>
</view>
<!-- 专业技能标签 -->
<view class="skills-preview">
<view wx:for="{{item.skills}}" wx:key="index" wx:if="{{index < 3}}" class="skill-tag-small">
{{item}}</view>
<view wx:if="{{item.skills.length > 3}}" class="skill-more">
+{{item.skills.length - 3}}</view>
</view>
</view>
</view>
<view class="action-buttons">
<view class="button-chat" bindtap="onChat" data-id="{{item.id}}">
<text class="button-icon">💬</text>
</view>
<view class="button-call" bindtap="onCall" data-phone="{{item.phoneNumber}}">
<text class="button-icon">📞</text>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<text>👤</text>
<text class="empty-text">暂无匹配的经纪人</text>
</view>
</view>
</view>

383
pages/customer-service/index.wxss

@ -0,0 +1,383 @@
/* pages/customer-service/index.wxss */
.container {
padding-bottom: 100rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 44rpx 30rpx 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
width: 100%;
box-sizing: border-box;
}
.nav-left {
width: 80rpx;
display: flex;
justify-content: flex-start;
}
.back-icon {
font-size: 32rpx;
color: #333;
font-weight: normal;
}
.nav-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
.nav-right {
display: flex;
align-items: center;
gap: 30rpx;
}
.search-icon, .settings-icon {
font-size: 32rpx;
color: #333;
}
/* 搜索区域 */
.search-container {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 10rpx solid #f8f8f8;
position: fixed;
top: 110rpx;
left: 0;
right: 0;
z-index: 999;
width: 100%;
box-sizing: border-box;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 40rpx;
padding: 16rpx 24rpx;
margin-bottom: 20rpx;
transition: all 0.3s ease;
}
.search-bar:focus-within {
background-color: #e6f7ff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1);
}
.search-icon-small {
font-size: 28rpx;
color: #999;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
background: none;
padding: 0;
}
.search-input::placeholder {
color: #999;
}
.filter-area {
display: flex;
align-items: center;
}
.area-picker {
display: flex;
align-items: center;
background-color: #f5f5f5;
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 28rpx;
color: #333;
}
.picker-arrow {
margin-left: 8rpx;
font-size: 20rpx;
color: #999;
}
/* 经纪人列表 */
.broker-list {
background-color: #f8f8f8;
padding: 0 30rpx;
margin-top: 280rpx; /* 为固定导航和搜索区域留出空间 */
}
.broker-item {
background-color: #fff;
border-radius: 24rpx;
margin: 20rpx 0;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.broker-item:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
background-color: #fafafa;
}
.broker-info {
display: flex;
margin-bottom: 24rpx;
}
.avatar-container {
width: 100rpx;
height: 100rpx;
margin-right: 24rpx;
position: relative;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #f0f0f0;
border: 2rpx solid #f0f0f0;
}
.online-indicator {
position: absolute;
bottom: 0;
right: 0;
background-color: #52c41a;
color: white;
font-size: 18rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
border: 2rpx solid white;
}
.offline-indicator {
position: absolute;
bottom: 0;
right: 0;
background-color: #999;
color: white;
font-size: 18rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
border: 2rpx solid white;
}
.broker-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
flex-wrap: wrap;
}
.name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 12rpx;
}
.score-text {
font-size: 24rpx;
color: #fff;
background: linear-gradient(135deg, #ffb800, #ff7700);
padding: 4rpx 12rpx;
border-radius: 12rpx;
margin-right: 8rpx;
}
.online-status {
font-size: 22rpx;
color: #52c41a;
}
.company {
font-size: 28rpx;
color: #666;
margin-bottom: 6rpx;
line-height: 1.4;
}
.department {
font-size: 26rpx;
color: #999;
margin-bottom: 6rpx;
line-height: 1.4;
}
.area {
font-size: 26rpx;
color: #333;
margin-bottom: 6rpx;
line-height: 1.4;
font-weight: 500;
}
.experience {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
/* 专业技能标签预览 */
.skills-preview {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 12rpx;
}
.skill-tag-small {
background-color: #f0f9ff;
color: #1890ff;
font-size: 22rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
border: 1rpx solid #bae7ff;
}
.skill-more {
background-color: #f5f5f5;
color: #999;
font-size: 22rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
border: 1rpx solid #e0e0e0;
}
/* 业绩数据统计样式 */
.performance-stats {
display: flex;
justify-content: space-between;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-value {
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
}
.stat-value.profit-rate {
color: #52c41a;
}
.stat-label {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
display: block;
}
.stat-divider {
color: #e0e0e0;
font-size: 24rpx;
margin: 0 10rpx;
display: flex;
align-items: center;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.button-chat, .button-call {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.button-chat {
background-color: #e6f7f0;
}
.button-call {
background-color: #e6f0f7;
}
.button-chat:active {
transform: scale(0.95);
opacity: 0.9;
background-color: #c3e6cb;
}
.button-call:active {
transform: scale(0.95);
opacity: 0.9;
background-color: #b3d8ff;
}
.button-icon {
font-size: 36rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
background-color: #fff;
margin: 20rpx 0;
border-radius: 24rpx;
}
.empty-state text:first-child {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}

591
pages/evaluate/index.js

@ -1,235 +1,186 @@
// pages/evaluate/index.js
//估价暂未开放
//估价页面
const api = require('../../utils/api.js');
Page({
data: {
evaluateStep: 1,
fromPreviousStep: false, // 用于标记是否从下一步返回
evaluateData: {
region: '',
type: '',
brand: '',
model: '',
breed: '',
spec: '',
freshness: '',
size: '',
packaging: '',
spec: ''
quantity: ''
},
// 客户地区列表 - 省市二级联动数据
provinceCities: {
'北京': ['北京'],
'河北': ['石家庄', '唐山', '秦皇岛', '邯郸', '邢台', '保定', '张家口', '承德', '沧州', '廊坊', '衡水'],
'四川': ['成都', '自贡', '攀枝花', '泸州', '德阳', '绵阳', '广元', '遂宁', '内江', '乐山', '南充', '眉山', '宜宾', '广安', '达州', '雅安', '巴中', '资阳'],
'云南': ['昆明', '曲靖', '玉溪', '保山', '昭通', '丽江', '普洱', '临沧', '楚雄', '红河', '文山', '西双版纳', '大理', '德宏', '怒江', '迪庆'],
'贵州': ['贵阳', '六盘水', '遵义', '安顺', '毕节', '铜仁', '黔西南', '黔东南', '黔南']
},
// 客户地区列表
regions: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安', '南京', '重庆'],
// 省份列表
provinces: [],
// 城市列表
cities: [],
// 当前选中的省份
selectedProvince: '',
// 当前是否显示城市选择
showCities: false,
evaluateResult: {
finalPrice: '0',
totalPrice: '0'
totalPrice: '0',
aidate: ''
},
// 鸡蛋类型数据(包含成交单量)
eggTypes: [
{ name: '土鸡蛋', dailyOrders: 1258, desc: '散养鸡产出的优质鸡蛋' },
{ name: '洋鸡蛋', dailyOrders: 3421, desc: '规模化养殖的普通鸡蛋' },
{ name: '乌鸡蛋', dailyOrders: 892, desc: '乌鸡产出的特色鸡蛋' },
{ name: '有机鸡蛋', dailyOrders: 675, desc: '有机认证的高品质鸡蛋' },
{ name: '初生蛋', dailyOrders: 965, desc: '母鸡产的第一窝鸡蛋' }
// 鸡蛋品种数据(包含成交单量)
eggBreeds: [
{ name: '罗曼粉', dailyOrders: 2341 },
{ name: '伊莎粉', dailyOrders: 1892 },
{ name: '罗曼灰', dailyOrders: 1567 },
{ name: '海蓝灰', dailyOrders: 1432 },
{ name: '海蓝褐', dailyOrders: 1298 },
{ name: '绿壳', dailyOrders: 1054 },
{ name: '粉一', dailyOrders: 987 },
{ name: '粉二', dailyOrders: 876 },
{ name: '粉八', dailyOrders: 765 },
{ name: '京粉1号', dailyOrders: 654 },
{ name: '京红', dailyOrders: 543 },
{ name: '京粉6号', dailyOrders: 456 },
{ name: '京粉3号', dailyOrders: 389 },
{ name: '农大系列', dailyOrders: 321 },
{ name: '黑鸡土蛋', dailyOrders: 298 },
{ name: '双黄蛋', dailyOrders: 245 },
{ name: '大午金凤', dailyOrders: 198 },
{ name: '黑凤', dailyOrders: 156 }
],
// 鸡蛋品牌和型号数据(包含成交单量)
eggData: {
'土鸡蛋': {
brands: [
{ name: '农家乐', dailyOrders: 456 },
{ name: '山野', dailyOrders: 389 },
{ name: '生态园', dailyOrders: 243 },
{ name: '田园', dailyOrders: 170 }
],
models: {
'农家乐': [
{ name: '散养土鸡蛋', dailyOrders: 213 },
{ name: '林下土鸡蛋', dailyOrders: 132 },
{ name: '谷物喂养土鸡蛋', dailyOrders: 78 },
{ name: '农家土鸡蛋', dailyOrders: 33 }
],
'山野': [
{ name: '高山散养土鸡蛋', dailyOrders: 189 },
{ name: '林间土鸡蛋', dailyOrders: 124 },
{ name: '野生土鸡蛋', dailyOrders: 76 }
],
'生态园': [
{ name: '有机土鸡蛋', dailyOrders: 112 },
{ name: '无抗土鸡蛋', dailyOrders: 89 },
{ name: '生态土鸡蛋', dailyOrders: 42 }
],
'田园': [
{ name: '农家土鸡蛋', dailyOrders: 87 },
{ name: '走地鸡蛋', dailyOrders: 54 },
{ name: '自然放养土鸡蛋', dailyOrders: 29 }
]
}
},
'洋鸡蛋': {
brands: [
{ name: '德青源', dailyOrders: 1234 },
{ name: '圣迪乐村', dailyOrders: 987 },
{ name: '正大', dailyOrders: 765 },
{ name: '咯咯哒', dailyOrders: 435 }
],
models: {
'德青源': [
{ name: '安心鲜鸡蛋', dailyOrders: 543 },
{ name: '谷物鸡蛋', dailyOrders: 456 },
{ name: '营养鸡蛋', dailyOrders: 235 }
],
'圣迪乐村': [
{ name: '高品质鲜鸡蛋', dailyOrders: 432 },
{ name: '谷物鸡蛋', dailyOrders: 321 },
{ name: '生态鸡蛋', dailyOrders: 234 }
],
'正大': [
{ name: '鲜鸡蛋', dailyOrders: 345 },
{ name: '营养鸡蛋', dailyOrders: 243 },
{ name: '优选鸡蛋', dailyOrders: 177 }
],
'咯咯哒': [
{ name: '鲜鸡蛋', dailyOrders: 213 },
{ name: '谷物鸡蛋', dailyOrders: 145 },
{ name: '农家鸡蛋', dailyOrders: 77 }
]
}
},
'乌鸡蛋': {
brands: [
{ name: '山野', dailyOrders: 345 },
{ name: '生态园', dailyOrders: 289 },
{ name: '农家乐', dailyOrders: 258 }
],
models: {
'山野': [
{ name: '散养乌鸡蛋', dailyOrders: 156 },
{ name: '林下乌鸡蛋', dailyOrders: 102 },
{ name: '野生乌鸡蛋', dailyOrders: 87 }
],
'生态园': [
{ name: '有机乌鸡蛋', dailyOrders: 123 },
{ name: '无抗乌鸡蛋', dailyOrders: 98 },
{ name: '生态乌鸡蛋', dailyOrders: 68 }
],
'农家乐': [
{ name: '农家乌鸡蛋', dailyOrders: 112 },
{ name: '谷物乌鸡蛋', dailyOrders: 93 },
{ name: '散养乌鸡蛋', dailyOrders: 53 }
]
}
},
'有机鸡蛋': {
brands: [
{ name: '生态园', dailyOrders: 289 },
{ name: '山野', dailyOrders: 213 },
{ name: '田园', dailyOrders: 173 }
],
models: {
'生态园': [
{ name: '有机认证鸡蛋', dailyOrders: 132 },
{ name: '无抗有机鸡蛋', dailyOrders: 98 },
{ name: '生态有机鸡蛋', dailyOrders: 59 }
],
'山野': [
{ name: '有机散养鸡蛋', dailyOrders: 98 },
{ name: '有机谷物鸡蛋', dailyOrders: 76 },
{ name: '野生有机鸡蛋', dailyOrders: 39 }
],
'田园': [
{ name: '有机农家鸡蛋', dailyOrders: 89 },
{ name: '有机初生蛋', dailyOrders: 54 },
{ name: '自然有机鸡蛋', dailyOrders: 30 }
]
}
},
'初生蛋': {
brands: [
{ name: '农家乐', dailyOrders: 342 },
{ name: '山野', dailyOrders: 312 },
{ name: '生态园', dailyOrders: 311 }
],
models: {
'农家乐': [
{ name: '土鸡初生蛋', dailyOrders: 156 },
{ name: '散养初生蛋', dailyOrders: 112 },
{ name: '农家初生蛋', dailyOrders: 74 }
],
'山野': [
{ name: '高山初生蛋', dailyOrders: 145 },
{ name: '林下初生蛋', dailyOrders: 98 },
{ name: '野生初生蛋', dailyOrders: 69 }
],
'生态园': [
{ name: '有机初生蛋', dailyOrders: 134 },
{ name: '无抗初生蛋', dailyOrders: 102 },
{ name: '生态初生蛋', dailyOrders: 75 }
// 鸡蛋规格数据
eggSpecs: [
{ name: '格子装', dailyOrders: 3214 },
{ name: '散托', dailyOrders: 2890 },
{ name: '不限规格', dailyOrders: 2567 },
{ name: '净重47+', dailyOrders: 2345 },
{ name: '净重46-47', dailyOrders: 2109 },
{ name: '净重45-46', dailyOrders: 1876 },
{ name: '净重44-45', dailyOrders: 1654 },
{ name: '净重43-44', dailyOrders: 1432 },
{ name: '净重42-43', dailyOrders: 1234 },
{ name: '净重41-42', dailyOrders: 1056 },
{ name: '净重40-41', dailyOrders: 987 },
{ name: '净重39-40', dailyOrders: 876 },
{ name: '净重38-39', dailyOrders: 765 },
{ name: '净重37-39', dailyOrders: 654 },
{ name: '净重37-38', dailyOrders: 543 },
{ name: '净重36-38', dailyOrders: 456 },
{ name: '净重36-37', dailyOrders: 389 },
{ name: '净重35-36', dailyOrders: 321 },
{ name: '净重34-35', dailyOrders: 289 },
{ name: '净重33-34', dailyOrders: 256 },
{ name: '净重32-33', dailyOrders: 223 },
{ name: '净重32-34', dailyOrders: 198 },
{ name: '净重31-32', dailyOrders: 165 },
{ name: '净重30-35', dailyOrders: 143 },
{ name: '净重30-34', dailyOrders: 121 },
{ name: '净重30-32', dailyOrders: 109 },
{ name: '净重30-31', dailyOrders: 98 },
{ name: '净重29-31', dailyOrders: 87 },
{ name: '净重29-30', dailyOrders: 76 },
{ name: '净重28-29', dailyOrders: 65 },
{ name: '净重28以下', dailyOrders: 54 },
{ name: '毛重52以上', dailyOrders: 132 },
{ name: '毛重50-51', dailyOrders: 121 },
{ name: '毛重48-49', dailyOrders: 109 },
{ name: '毛重47-48', dailyOrders: 98 },
{ name: '毛重46-47', dailyOrders: 87 },
{ name: '毛重45-47', dailyOrders: 76 },
{ name: '毛重45-46', dailyOrders: 65 },
{ name: '毛重44-45', dailyOrders: 54 },
{ name: '毛重43-44', dailyOrders: 43 },
{ name: '毛重42-43', dailyOrders: 32 },
{ name: '毛重41-42', dailyOrders: 21 },
{ name: '毛重40-41', dailyOrders: 19 },
{ name: '毛重38-39', dailyOrders: 17 },
{ name: '毛重36-37', dailyOrders: 15 },
{ name: '毛重34-35', dailyOrders: 13 },
{ name: '毛重32-33', dailyOrders: 11 },
{ name: '毛重30-31', dailyOrders: 9 },
{ name: '毛重30以下', dailyOrders: 7 }
]
}
}
},
eggBrands: [],
eggModels: []
},
onLoad() {
console.log('估价页面初始化')
// 页面加载时,对鸡蛋类型按成交单量降序排序并添加排名
const sortedTypes = [...this.data.eggTypes].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 页面加载时,对鸡蛋品种按成交单量降序排序并添加排名
const sortedBreeds = [...this.data.eggBreeds].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const typesWithRank = sortedTypes.map((type, index) => ({
...type,
const breedsWithRank = sortedBreeds.map((breed, index) => ({
...breed,
rank: index + 1
}));
// 对鸡蛋规格按成交单量降序排序并添加排名
const sortedSpecs = [...this.data.eggSpecs].sort((a, b) => b.dailyOrders - a.dailyOrders);
const specsWithRank = sortedSpecs.map((spec, index) => ({
...spec,
rank: index + 1
}));
// 初始化省份列表
const provinces = Object.keys(this.data.provinceCities);
this.setData({
eggTypes: typesWithRank,
fromPreviousStep: false // 初始化标志
eggBreeds: breedsWithRank,
eggSpecs: specsWithRank,
fromPreviousStep: false, // 初始化标志
provinces: provinces
});
},
// 选择客户地区
selectRegion(e) {
const region = e.currentTarget.dataset.region;
// 选择省份
selectProvince(e) {
const province = e.currentTarget.dataset.province;
const cities = this.data.provinceCities[province];
this.setData({
'evaluateData.region': region
selectedProvince: province,
cities: cities,
showCities: true
});
},
// 只有当当前步骤是1且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 1 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 2
// 选择城市
selectCity(e) {
const city = e.currentTarget.dataset.city;
if (!city || !this.data.selectedProvince) {
wx.showToast({
title: '请选择有效的城市',
icon: 'none',
duration: 2000
});
return;
}
// 重置标志
const fullRegion = `${this.data.selectedProvince}-${city}`;
this.setData({
fromPreviousStep: false
'evaluateData.region': fullRegion,
showCities: false
});
},
// 选择鸡蛋类型
selectEggType(e) {
const type = e.currentTarget.dataset.type;
// 获取该类型下的品牌,并按成交单量降序排序
const brands = [...this.data.eggData[type].brands].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const brandsWithRank = brands.map((brand, index) => ({
...brand,
rank: index + 1
}));
this.setData({
'evaluateData.type': type,
eggBrands: brandsWithRank,
// 清除之前选择的品牌和型号
'evaluateData.brand': '',
'evaluateData.model': '',
eggModels: []
// 显示选择成功提示
wx.showToast({
title: `已选择: ${fullRegion}`,
icon: 'success',
duration: 1500
});
// 只有当当前步骤是2且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 2 && !this.data.fromPreviousStep) {
// 只有当当前步骤是1且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 1 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 3
evaluateStep: 2
});
}
@ -239,56 +190,57 @@ Page({
});
},
// 选择鸡蛋品牌
selectEggBrand(e) {
const brand = e.currentTarget.dataset.brand;
const type = this.data.evaluateData.type;
// 获取该品牌下的型号,并按成交单量降序排序
const models = [...this.data.eggData[type].models[brand]].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const modelsWithRank = models.map((model, index) => ({
...model,
rank: index + 1
}));
// 返回选择省份
backToProvince() {
this.setData({
'evaluateData.brand': brand,
eggModels: modelsWithRank,
// 清除之前选择的型号
'evaluateData.model': ''
showCities: false
});
},
// 只有当当前步骤是3且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 3 && !this.data.fromPreviousStep) {
// 选择鸡蛋品种
selectEggBreed(e) {
const breed = e.currentTarget.dataset.breed;
this.setData({
evaluateStep: 4
'evaluateData.breed': breed
});
// 显示选择成功提示
wx.showToast({
title: '已选择: ' + breed,
icon: 'none',
duration: 1500
});
// 只有当当前步骤是2且不是从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 2 && !this.data.fromPreviousStep) {
this.setData({ evaluateStep: 3 });
}
// 重置标志
this.setData({
fromPreviousStep: false
});
this.setData({ fromPreviousStep: false });
},
// 选择鸡蛋型号
selectEggModel(e) {
const model = e.currentTarget.dataset.model;
// 选择鸡蛋规格
selectEggSpec(e) {
const spec = e.currentTarget.dataset.spec;
this.setData({
'evaluateData.model': model
'evaluateData.spec': spec
});
// 只有当当前步骤是4且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 4 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 5
// 显示选择成功提示
wx.showToast({
title: '已选择: ' + spec,
icon: 'none',
duration: 1500
});
// 只有当当前步骤是3且不是从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 3 && !this.data.fromPreviousStep) {
this.setData({ evaluateStep: 4 });
}
// 重置标志
this.setData({
fromPreviousStep: false
});
this.setData({ fromPreviousStep: false });
},
// 格式化订单数量显示
@ -312,36 +264,34 @@ Page({
if (!this.data.fromPreviousStep) {
// 根据当前步骤自动进入下一步
const currentStep = this.data.evaluateStep;
if (currentStep === 5) {
if (currentStep === 4) {
this.setData({ evaluateStep: 5 });
} else if (currentStep === 5) {
this.setData({ evaluateStep: 6 });
} else if (currentStep === 6) {
this.setData({ evaluateStep: 7 });
} else if (currentStep === 7) {
this.setData({ evaluateStep: 8 });
}
}
// 重置标志
this.setData({
fromPreviousStep: false
});
this.setData({ fromPreviousStep: false });
},
// 选择规格
selectSpec(e) {
const spec = e.currentTarget.dataset.spec;
// 选择数量
selectQuantity(e) {
const quantity = e.currentTarget.dataset.quantity;
this.setData({
'evaluateData.spec': spec
'evaluateData.quantity': quantity
});
},
// 获取报价
getQuote() {
if (this.data.evaluateData.spec) {
if (this.data.evaluateData.quantity) {
this.calculatePrice();
} else {
wx.showToast({
title: '请选择规格',
title: '请选择数量',
icon: 'none',
duration: 2000
});
@ -350,11 +300,17 @@ Page({
// 上一步
prevStep() {
if (this.data.evaluateStep > 1) {
if (this.data.evaluateStep > 1 && this.data.evaluateStep < 9) {
this.setData({
evaluateStep: this.data.evaluateStep - 1,
fromPreviousStep: true // 标记是从下一步返回的
});
} else if (this.data.evaluateStep === 9) {
// 如果在结果页面,返回第七步(数量选择)
this.setData({
evaluateStep: 7,
fromPreviousStep: true
});
} else {
// 如果在第一步,返回上一页
wx.navigateBack();
@ -363,10 +319,13 @@ Page({
// 计算价格
calculatePrice() {
const { region, type, brand, model, freshness, size, packaging, spec } = this.data.evaluateData;
console.log('开始执行calculatePrice函数');
const { region, breed, spec, freshness, size, packaging, quantity } = this.data.evaluateData;
// 校验必填参数
if (!region || !type || !brand || !model || !freshness || !size || !packaging || !spec) {
console.log('校验必填参数:', { region, breed, spec, freshness, size, packaging, quantity });
if (!region || !breed || !spec || !freshness || !size || !packaging || !quantity) {
console.error('参数校验失败,缺少必要信息');
wx.showToast({
title: '请完成所有选项',
icon: 'none',
@ -381,95 +340,91 @@ Page({
mask: true
});
// 模拟计算延迟
setTimeout(() => {
// 基础价格表(元/斤)
const basePrices = {
'土鸡蛋': 25,
'洋鸡蛋': 15,
'乌鸡蛋': 35,
'有机鸡蛋': 40,
'初生蛋': 45
};
// 整合用户输入信息
console.log('准备整合用户输入信息');
const userData = JSON.stringify({
region,
breed,
spec,
freshness,
size,
packaging,
quantity
});
console.log('用户输入信息:', userData);
// 向后端发送请求
console.log('准备发送API请求到 /api/openai/submit');
api.request('/api/openai/submit', 'POST', {
userdate: userData
}).then(res => {
console.log('API请求成功,响应数据:', res);
// 检查响应数据有效性
if (!res) {
throw new Error('后端返回的响应为空');
}
// 品牌溢价系数
const brandMultipliers = {
'农家乐': 1.0,
'山野': 1.1,
'生态园': 1.2,
'田园': 1.0,
'德青源': 1.1,
'圣迪乐村': 1.15,
'正大': 1.05,
'咯咯哒': 1.0
// 解析后端返回的aidate数据
let aiData;
try {
console.log('开始解析aidate数据:', res.aidate);
aiData = typeof res.aidate === 'string' ? JSON.parse(res.aidate) : res.aidate;
console.log('aidate解析结果:', aiData);
} catch (e) {
console.error('解析aidate失败:', e);
console.warn('使用默认价格数据');
// 如果解析失败,使用默认值
aiData = {
finalPrice: '0',
totalPrice: '0'
};
}
// 型号溢价系数
const modelMultipliers = {
// 土鸡蛋型号系数
'散养土鸡蛋': 1.1, '林下土鸡蛋': 1.15, '谷物喂养土鸡蛋': 1.2, '农家土鸡蛋': 1.0,
'高山散养土鸡蛋': 1.25, '林间土鸡蛋': 1.1, '野生土鸡蛋': 1.3,
'有机土鸡蛋': 1.3, '无抗土鸡蛋': 1.25, '生态土鸡蛋': 1.2,
'走地鸡蛋': 1.1, '自然放养土鸡蛋': 1.12,
// 洋鸡蛋型号系数
'安心鲜鸡蛋': 1.0, '谷物鸡蛋': 1.1, '营养鸡蛋': 1.05,
'高品质鲜鸡蛋': 1.15, '生态鸡蛋': 1.2,
'鲜鸡蛋': 1.0, '优选鸡蛋': 1.1,
'农家鸡蛋': 1.05,
// 乌鸡蛋型号系数
'散养乌鸡蛋': 1.1, '林下乌鸡蛋': 1.15, '野生乌鸡蛋': 1.3,
'有机乌鸡蛋': 1.3, '无抗乌鸡蛋': 1.25, '生态乌鸡蛋': 1.2,
'农家乌鸡蛋': 1.0, '谷物乌鸡蛋': 1.1,
// 有机鸡蛋型号系数
'有机认证鸡蛋': 1.3, '无抗有机鸡蛋': 1.35, '生态有机鸡蛋': 1.32,
'有机散养鸡蛋': 1.25, '有机谷物鸡蛋': 1.2, '野生有机鸡蛋': 1.4,
'有机农家鸡蛋': 1.1, '有机初生蛋': 1.4, '自然有机鸡蛋': 1.2,
// 初生蛋型号系数
'土鸡初生蛋': 1.2, '散养初生蛋': 1.25, '农家初生蛋': 1.15,
'高山初生蛋': 1.3, '林下初生蛋': 1.25, '野生初生蛋': 1.45,
'有机初生蛋': 1.4, '无抗初生蛋': 1.35, '生态初生蛋': 1.3
// 验证解析后的数据
if (!aiData || typeof aiData !== 'object') {
console.error('解析后的aidata格式错误:', aiData);
aiData = {
finalPrice: '0',
totalPrice: '0'
};
// 状况调整系数
const freshnessCoefficient = { '非常新鲜': 1.0, '较新鲜': 0.85, '一般': 0.7, '不新鲜': 0.4 };
const sizeCoefficient = { '特大': 1.3, '大': 1.1, '中': 1.0, '小': 0.8 };
const packagingCoefficient = { '原装完整': 1.0, '部分包装': 0.9, '散装': 0.8 };
// 计算单价(元/斤)
let unitPrice = basePrices[type] || 20;
const brandMultiplier = brandMultipliers[brand] || 1.0;
const modelMultiplier = modelMultipliers[model] || 1.0;
unitPrice = unitPrice * brandMultiplier * modelMultiplier;
unitPrice *= freshnessCoefficient[freshness];
unitPrice *= sizeCoefficient[size];
unitPrice *= packagingCoefficient[packaging];
// 确保价格合理
unitPrice = Math.max(unitPrice, 1);
// 计算总价(假设每个鸡蛋约0.05斤)
const eggsPerKilogram = 20; // 约20个鸡蛋/斤
const specCount = parseInt(spec) || 0;
const totalWeight = specCount / eggsPerKilogram;
const totalPrice = unitPrice * totalWeight;
}
// 更新结果
console.log('准备更新页面数据,finalPrice:', aiData.finalPrice || '0', 'totalPrice:', aiData.totalPrice || '0');
this.setData({
evaluateResult: {
finalPrice: unitPrice.toFixed(1),
totalPrice: totalPrice.toFixed(1)
finalPrice: aiData.finalPrice || '0',
totalPrice: aiData.totalPrice || '0',
aidate: res.aidate || '' // 保存完整的aidate数据
},
evaluateStep: 9
}, () => {
console.log('页面数据更新完成,加载状态隐藏');
wx.hideLoading();
});
}).catch(err => {
console.error('API请求异常:', err);
wx.hideLoading();
// 根据不同类型的错误提供更详细的提示
let errorMessage = '计算失败,请重试';
if (err && err.message) {
console.error('错误详情:', err.message);
// 可以根据具体错误类型提供更精确的提示
if (err.message.includes('网络')) {
errorMessage = '网络连接异常,请检查网络设置';
} else if (err.message.includes('超时')) {
errorMessage = '请求超时,请稍后再试';
}
}
}, 800);
wx.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
});
},
// 重新估价
@ -478,17 +433,17 @@ Page({
evaluateStep: 1,
evaluateData: {
region: '',
type: '',
brand: '',
model: '',
breed: '',
spec: '',
freshness: '',
size: '',
packaging: '',
spec: ''
quantity: ''
},
evaluateResult: {
finalPrice: '0',
totalPrice: '0'
totalPrice: '0',
aidate: ''
},
fromPreviousStep: false // 重置标志
});

138
pages/evaluate/index.wxml

@ -17,87 +17,91 @@
<view class="progress-dot {{evaluateStep >= 5 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 6 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 7 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 8 ? 'active' : ''}}"></view>
</view>
<view class="progress-text">步骤 {{evaluateStep}}/8</view>
<view class="progress-text">步骤 {{evaluateStep}}/7</view>
</view>
<!-- 步骤1:选择客户地区 -->
<view wx:if="{{evaluateStep === 1}}" class="evaluate-step">
<view class="step-content">
<!-- 省份选择 -->
<view wx:if="{{!showCities}}">
<view class="step-title">选择客户地区</view>
<view class="step-subtitle">请选择您所在的地区</view>
<view class="step-subtitle">请选择您所在的省份</view>
<view class="category-list">
<view wx:for="{{regions}}" wx:key="*this"
class="category-item {{evaluateData.region === item ? 'selected' : ''}}"
bindtap="selectRegion" data-region="{{item}}">
<view wx:for="{{provinces}}" wx:key="*this"
class="category-item"
bindtap="selectProvince" data-province="{{item}}">
<view class="category-info">
<view class="category-name">{{item}}</view>
<view class="category-desc">点击选择该地区</view>
<view class="category-desc">点击选择该省份</view>
</view>
<view class="category-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤2:选择鸡蛋类型 -->
<view wx:if="{{evaluateStep === 2}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择鸡蛋类型</view>
<view class="step-subtitle">请选择您要估价的鸡蛋类型(按每日成交单量排序)</view>
<!-- 城市选择 -->
<view wx:if="{{showCities}}">
<view class="step-title">选择城市</view>
<view class="step-subtitle">{{selectedProvince}} - 请选择具体城市</view>
<view class="city-header" bindtap="backToProvince">
<view class="city-back">‹ 返回省份选择</view>
</view>
<view class="category-list">
<view wx:for="{{eggTypes}}" wx:key="name" wx:for-item="eggType"
class="category-item {{evaluateData.type === eggType.name ? 'selected' : ''}}"
bindtap="selectEggType" data-type="{{eggType.name}}">
<view wx:for="{{cities}}" wx:key="*this"
class="category-item {{evaluateData.region === selectedProvince + '-' + item ? 'selected' : ''}}"
bindtap="selectCity" data-city="{{item}}">
<view class="category-info">
<view class="category-name">
<text class="rank-number rank-{{eggType.rank <= 3 ? eggType.rank : ''}}">{{eggType.rank}}</text>
{{eggType.name}}
</view>
<view class="category-desc">{{eggType.desc}}</view>
<view class="category-name">{{item}}</view>
<view class="category-desc">点击选择该城市</view>
</view>
<view class="category-arrow">›</view>
</view>
</view>
</view>
</view>
</view>
<!-- 步骤3:选择品牌 -->
<view wx:if="{{evaluateStep === 3}}" class="evaluate-step">
<!-- 步骤2:选择鸡蛋品种 -->
<view wx:if="{{evaluateStep === 2}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择品牌</view>
<view class="step-subtitle">{{evaluateData.type}} - 按每日成交单量排序</view>
<view class="step-title">选择鸡蛋品种</view>
<view class="step-subtitle">请选择您要估价的鸡蛋品种(按每日成交单量排序)</view>
<view class="option-list">
<view wx:for="{{eggBrands}}" wx:key="name" wx:for-item="brand"
class="option-item {{evaluateData.brand === brand.name ? 'selected' : ''}}"
bindtap="selectEggBrand" data-brand="{{brand.name}}">
<view class="option-text">
<text class="rank-number rank-{{brand.rank <= 3 ? brand.rank : ''}}">{{brand.rank}}</text>
{{brand.name}}
<view class="category-list">
<view wx:for="{{eggBreeds}}" wx:key="name" wx:for-item="eggBreed"
class="category-item {{evaluateData.breed === eggBreed.name ? 'selected' : ''}}"
bindtap="selectEggBreed" data-breed="{{eggBreed.name}}">
<view class="category-info">
<view class="category-name">
<text class="rank-number rank-{{eggBreed.rank <= 3 ? eggBreed.rank : ''}}">{{eggBreed.rank}}</text>
{{eggBreed.name}}
</view>
<view class="option-arrow">›</view>
<view class="category-desc">点击选择该品种</view>
</view>
<view class="category-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤4:选择具体型号 -->
<view wx:if="{{evaluateStep === 4}}" class="evaluate-step">
<!-- 步骤3:选择具体规格 -->
<view wx:if="{{evaluateStep === 3}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择具体型号</view>
<view class="step-subtitle">{{evaluateData.brand}} - 按每日成交单量排序</view>
<view class="step-title">选择具体规格</view>
<view class="step-subtitle">请选择鸡蛋的规格(按每日成交单量排序)</view>
<view class="option-list">
<view wx:for="{{eggModels}}" wx:key="name" wx:for-item="model"
class="option-item {{evaluateData.model === model.name ? 'selected' : ''}}"
bindtap="selectEggModel" data-model="{{model.name}}">
<view wx:for="{{eggSpecs}}" wx:key="name" wx:for-item="spec"
class="option-item {{evaluateData.spec === spec.name ? 'selected' : ''}}"
bindtap="selectEggSpec" data-spec="{{spec.name}}">
<view class="option-text">
<text class="rank-number rank-{{model.rank <= 3 ? model.rank : ''}}">{{model.rank}}</text>
{{model.name}}
<text class="rank-number rank-{{spec.rank <= 3 ? spec.rank : ''}}">{{spec.rank}}</text>
{{spec.name}}
</view>
<view class="option-arrow">›</view>
</view>
@ -105,8 +109,8 @@
</view>
</view>
<!-- 步骤5:新鲜度选择 -->
<view wx:if="{{evaluateStep === 5}}" class="evaluate-step">
<!-- 步骤4:新鲜度选择 -->
<view wx:if="{{evaluateStep === 4}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">新鲜程度</view>
<view class="step-subtitle">请选择鸡蛋的新鲜程度</view>
@ -151,8 +155,8 @@
</view>
</view>
<!-- 步骤6:大小选择 -->
<view wx:if="{{evaluateStep === 6}}" class="evaluate-step">
<!-- 步骤5:大小选择 -->
<view wx:if="{{evaluateStep === 5}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">鸡蛋大小</view>
<view class="step-subtitle">请选择鸡蛋的大小规格</view>
@ -197,8 +201,8 @@
</view>
</view>
<!-- 步骤7:包装情况 -->
<view wx:if="{{evaluateStep === 7}}" class="evaluate-step">
<!-- 步骤6:包装情况 -->
<view wx:if="{{evaluateStep === 6}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">包装情况</view>
<view class="step-subtitle">请选择鸡蛋的包装完好程度</view>
@ -234,26 +238,26 @@
</view>
</view>
<!-- 步骤8:选择规格 -->
<view wx:if="{{evaluateStep === 8}}" class="evaluate-step">
<!-- 步骤7:数量选择 -->
<view wx:if="{{evaluateStep === 7}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">请选择规格(数量</view>
<view class="step-subtitle">请选择鸡蛋的数量规格</view>
<view class="step-title">请选择数量</view>
<view class="step-subtitle">请选择鸡蛋的数量</view>
<view class="option-list">
<view class="option-item {{evaluateData.spec === '500' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="500">
<view class="option-item {{evaluateData.quantity === '500' ? 'selected' : ''}}" bindtap="selectQuantity" data-quantity="500">
<view class="option-text">500个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '1000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="1000">
<view class="option-item {{evaluateData.quantity === '1000' ? 'selected' : ''}}" bindtap="selectQuantity" data-quantity="1000">
<view class="option-text">1000个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '2000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="2000">
<view class="option-item {{evaluateData.quantity === '2000' ? 'selected' : ''}}" bindtap="selectQuantity" data-quantity="2000">
<view class="option-text">2000个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '10000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="10000">
<view class="option-item {{evaluateData.quantity === '10000' ? 'selected' : ''}}" bindtap="selectQuantity" data-quantity="10000">
<view class="option-text">10000个</view>
<view class="option-arrow">›</view>
</view>
@ -281,17 +285,25 @@
<text class="price-symbol">¥</text>
<text class="price-number">{{evaluateResult.totalPrice}}</text>
</view>
<view class="price-unit">元({{evaluateData.spec}}个)</view>
<view class="price-unit">元({{evaluateData.quantity}}个)</view>
</view>
</view>
<view class="result-content">
<!-- AI报价信息 -->
<view class="ai-quotation" wx:if="{{evaluateResult.aidate}}">
<view class="ai-quotation-title">
<text class="ai-icon">🤖</text>
AI智能报价
</view>
<view class="ai-quotation-content">{{evaluateResult.aidate}}</view>
</view>
<!-- 商品信息 -->
<view class="product-info-card">
<view class="product-type">{{evaluateData.type}}</view>
<view class="product-type">{{evaluateData.breed}}</view>
<view class="product-details">
<view class="product-brand">{{evaluateData.brand}}</view>
<view class="product-model">{{evaluateData.model}}</view>
<view class="product-spec">{{evaluateData.spec}}</view>
</view>
</view>
@ -313,7 +325,11 @@
</view>
<view class="condition-item">
<view class="condition-label">规格</view>
<view class="condition-value">{{evaluateData.spec}}个</view>
<view class="condition-value">{{evaluateData.spec}}</view>
</view>
<view class="condition-item">
<view class="condition-label">数量</view>
<view class="condition-value">{{evaluateData.quantity}}个</view>
</view>
<view class="condition-item">
<view class="condition-label">单价</view>

21
pages/evaluate/index.wxss

@ -124,6 +124,27 @@
line-height: 42rpx;
}
/* 城市选择头部返回按钮 */
.city-header {
margin-bottom: 40rpx;
padding: 20rpx 0;
}
.city-back {
font-size: 32rpx;
color: #666;
display: inline-flex;
align-items: center;
padding: 10rpx 16rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
}
.city-back:active {
background: #f5f5f5;
color: #FF6B00;
}
/* 类别列表 */
.category-list {
background: white;

2
pages/goods-detail/goods-detail.js

@ -79,7 +79,7 @@ Page({
const formattedGoods = {
id: productIdStr,
productId: productIdStr,
name: product.productName,
name: product.productName || product.name,
price: product.price,
minOrder: product.minOrder || product.quantity,
yolk: product.yolk,

2
pages/goods-detail/goods-detail.wxml

@ -31,7 +31,9 @@
<!-- 商品基本信息 -->
<view class="goods-info">
<view style="display: flex; align-items: center;">
<text class="goods-name">{{goodsDetail.name}}</text>
</view>
<view class="goods-price">
<text class="price-symbol">价格:</text>
<text class="price-value">{{goodsDetail.price}}</text>

131
pages/index/index.js

@ -12,6 +12,23 @@ Page({
testMode: true
},
// 跳转到聊天页面
navigateToChat() {
wx.navigateTo({
url: '/pages/chat/index',
success: function() {
console.log('成功跳转到聊天页面');
},
fail: function(error) {
console.error('跳转到聊天页面失败:', error);
wx.showToast({
title: '跳转失败,请稍后重试',
icon: 'none'
});
}
});
},
onLoad() {
console.log('首页初始化')
},
@ -29,69 +46,6 @@ Page({
app.updateCurrentTab('index');
},
// 选择买家身份
async chooseBuyer() {
// 买家不需要登录验证,直接跳转到买家页面
this.finishSetUserType('buyer');
},
// 选择卖家身份
async chooseSeller() {
this.finishSetUserType('seller');
},
// 检查登录状态并继续操作
checkLoginAndProceed(type) {
// 检查本地存储的登录信息
const openid = wx.getStorageSync('openid');
const userId = wx.getStorageSync('userId');
const userInfo = wx.getStorageSync('userInfo');
if (openid && userId && userInfo) {
console.log('用户已登录,直接处理身份选择');
// 用户已登录,直接处理
if (type === 'buyer') {
this.finishSetUserType(type);
} else if (type === 'seller') {
this.handleSellerRoute();
}
} else {
console.log('用户未登录,显示登录弹窗');
// 用户未登录,显示一键登录弹窗
this.setData({
pendingUserType: type,
showOneKeyLoginModal: true
});
}
},
// 处理卖家路由逻辑
async handleSellerRoute() {
try {
wx.switchTab({ url: '/pages/seller/index' });
// 查询用户信息获取partnerstatus字段
const userInfo = await API.getUserInfo();
if (userInfo && userInfo.data && userInfo.data.partnerstatus) {
// 将partnerstatus存储到本地
wx.setStorageSync('partnerstatus', userInfo.data.partnerstatus);
console.log('获取到的partnerstatus:', userInfo.data.partnerstatus);
}
// 无论partnerstatus是什么,都直接进入seller/index页面
wx.switchTab({ url: '/pages/seller/index' });
} catch (error) {
console.error('获取用户信息失败:', error);
// 出错时也直接进入seller/index页面
wx.switchTab({ url: '/pages/seller/index' });
}
},
// 跳转到估价页面
@ -113,6 +67,24 @@ Page({
this.setData({ showOneKeyLoginModal: false })
},
// 分享给朋友/群聊
onShareAppMessage() {
return {
title: '鸡蛋贸易平台 - 专业的鸡蛋交易小程序',
path: '/pages/index/index',
imageUrl: '/images/你有好蛋.png'
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '鸡蛋贸易平台 - 专业的鸡蛋交易小程序',
query: '',
imageUrl: '/images/你有好蛋.png'
}
},
// 处理手机号授权
async onGetPhoneNumber(e) {
// 打印详细错误信息,方便调试
@ -342,8 +314,7 @@ Page({
})
}
// 完成设置并跳转
this.finishSetUserType(currentUserType)
// 用户登录成功,但已移除类型选择和跳转功能
} else {
// 用户拒绝授权或其他情况
console.log('手机号授权失败:', e.detail.errMsg)
@ -463,8 +434,7 @@ Page({
duration: 2000
})
// 10. 完成设置并跳转
this.finishSetUserType(currentUserType)
// 测试登录成功,但已移除类型选择和跳转功能
} catch (error) {
wx.hideLoading()
console.error('测试模式登录过程中发生错误:', error)
@ -642,10 +612,9 @@ Page({
async processUserInfoAuth(type) {
const app = getApp()
// 如果已经有用户信息,直接完成设置并跳转
// 如果已经有用户信息,不需要再进行跳转
if (app.globalData.userInfo) {
wx.hideLoading()
this.finishSetUserType(type)
return
}
@ -675,8 +644,7 @@ Page({
// 隐藏加载提示
wx.hideLoading()
// 数据上传完成后再跳转
this.finishSetUserType(type)
// 数据上传完成,但已移除类型选择和跳转功能
} catch (error) {
console.error('处理用户信息授权失败:', error)
wx.hideLoading()
@ -804,8 +772,7 @@ Page({
showUserInfoForm: false
})
// 完成设置并跳转
this.finishSetUserType(type)
// 已移除类型选择和跳转功能
},
// 取消用户信息表单
@ -913,25 +880,9 @@ Page({
}
},
// 完成用户类型设置并跳转
finishSetUserType(type) {
console.log('用户类型设置完成,准备跳转到', type === 'buyer' ? '买家页面' : '卖家页面')
// 无论是否登录,直接跳转到对应页面
setTimeout(() => {
if (type === 'buyer') {
wx.switchTab({ url: '/pages/buyer/index' })
} else {
// 卖家身份直接跳转到卖家页面,不需要处理partnerstatus逻辑
wx.switchTab({ url: '/pages/seller/index' })
}
}, 500)
},
// 前往个人中心
toProfile() {
wx.switchTab({ url: '/pages/profile/index' })
}
})

16
pages/index/index.wxml

@ -1,22 +1,12 @@
<view class="container">
<image src="/images/生成鸡蛋贸易平台图片.png" style="width: 100%; height: 425rpx; margin: -280rpx auto 20rpx; display: block;"></image>
<view class="title" style="margin-top: 60rpx;">中国最专业的鸡蛋现货交易平台</view>
<button class="btn message-btn" bindtap="navigateToChat">消息中心</button>
<view class="desc" style="margin: 30rpx 0; text-align: center; padding: 0 20rpx;">
</view>
<!-- 身份选择 -->
<view style="text-align: center; margin-top: 30rpx; margin-bottom: 30rpx;">
<text style="font-size: 28rpx; color: #666; display: block; margin-bottom: 15rpx;"></text>
<button class="btn buyer-btn" bindtap="chooseBuyer">
我要买蛋
</button>
<button class="btn seller-btn" bindtap="chooseSeller">
我要卖蛋
</button>
</view>
<!-- 未授权登录提示弹窗 -->
<view wx:if="{{showAuthModal}}" class="auth-modal-overlay">
@ -76,7 +66,5 @@
</view>
</view>
<view class="login-hint">
已有账号?<text class="link-text" bindtap="toProfile">进入我的页面</text>
</view>
</view>

7
pages/index/index.wxss

@ -68,6 +68,13 @@ page {
background: rgba(76, 175, 80, 0.15);
}
/* 消息按钮样式 */
.message-btn {
color: #FF6B6B;
background: rgba(255, 107, 107, 0.15);
margin-top: 30rpx;
}
/* 立即入驻按钮样式 */
.settlement-btn {
color: #2196F3;

326
pages/message-list/index.js

@ -0,0 +1,326 @@
// 消息列表页面
Page({
data: {
messageList: []
},
onLoad: function() {
this.loadChatList();
// 注册全局新消息处理函数
this.registerGlobalMessageHandler();
},
/**
* 注册全局新消息处理函数
*/
registerGlobalMessageHandler: function() {
try {
const app = getApp();
const that = this;
// 保存原有的处理函数(如果有)
this.originalMessageHandler = app.globalData.onNewMessage;
// 注册新的处理函数
app.globalData.onNewMessage = function(message) {
console.log('消息列表页面收到新消息:', message);
// 重新加载聊天列表
that.loadChatList();
// 调用原始处理函数(如果有)
if (that.originalMessageHandler && typeof that.originalMessageHandler === 'function') {
that.originalMessageHandler(message);
}
};
console.log('已注册全局新消息处理函数');
} catch (e) {
console.error('注册全局新消息处理函数失败:', e);
}
},
onShow: function() {
// 每次显示页面时刷新聊天列表
this.loadChatList();
},
/**
* 加载聊天列表
*/
loadChatList: function() {
try {
// 获取所有存储的聊天记录键
const storageInfo = wx.getStorageInfoSync();
const chatKeys = storageInfo.keys.filter(key => key.startsWith('chat_messages_'));
const messageList = [];
// 获取当前用户信息
const app = getApp();
const userInfo = app.globalData.userInfo || {};
const isCurrentUserManager = userInfo.userType === 'manager' || userInfo.type === 'manager';
const currentManagerId = userInfo.managerId || '';
const currentUserId = wx.getStorageSync('userId') || '';
// 临时存储已处理的会话,避免重复
const processedConversations = new Set();
// 遍历每个聊天记录
chatKeys.forEach(key => {
const chatUserId = key.replace('chat_messages_', '');
// 获取消息列表
const messages = wx.getStorageSync(key);
// 跳过空消息列表
if (!messages || messages.length === 0) return;
// 对于客服,需要特殊处理,确保能看到所有相关消息
if (isCurrentUserManager) {
// 避免处理自己与自己的聊天
if (chatUserId === currentManagerId) return;
// 客服需要看到所有有消息的对话,包括用户发送的消息
if (!processedConversations.has(chatUserId)) {
// 获取最后一条消息
const lastMessage = messages[messages.length - 1];
messageList.push({
userId: chatUserId,
userName: this.getUserNameById(chatUserId),
avatar: '',
lastMessage: this.formatMessagePreview(lastMessage),
lastMessageTime: this.formatMessageTime(lastMessage.time),
messageCount: messages.length
});
processedConversations.add(chatUserId);
}
} else {
// 普通用户,正常处理
// 避免处理自己与自己的聊天
if (chatUserId === currentUserId) return;
// 获取最后一条消息
const lastMessage = messages[messages.length - 1];
messageList.push({
userId: chatUserId,
userName: this.getUserNameById(chatUserId),
avatar: '',
lastMessage: this.formatMessagePreview(lastMessage),
lastMessageTime: this.formatMessageTime(lastMessage.time),
messageCount: messages.length
});
}
});
// 按最后消息时间排序(最新的在前)
messageList.sort((a, b) => {
return new Date(b.lastMessageTime) - new Date(a.lastMessageTime);
});
this.setData({
messageList: messageList
});
} catch (e) {
console.error('加载聊天列表失败:', e);
}
},
/**
* 根据用户ID获取用户名
*/
getUserNameById: function(userId) {
try {
// 参数有效性检查
if (!userId || typeof userId === 'undefined') {
return '未知用户';
}
// 确保userId是字符串类型
const safeUserId = String(userId);
// 尝试从全局客服列表中获取用户名
const app = getApp();
// 尝试从本地缓存的客服列表中查找
const cachedCustomerServices = wx.getStorageSync('cached_customer_services') || [];
const service = cachedCustomerServices.find(item =>
item.id === safeUserId || item.managerId === safeUserId ||
String(item.id) === safeUserId || String(item.managerId) === safeUserId
);
if (service) {
return service.alias || service.name || '客服';
}
// 固定用户名映射
const userMap = {
'user1': '客服专员',
'user2': '商家客服',
'user_1': '张三',
'user_2': '李四',
'user_3': '王五',
'user_4': '赵六',
'user_5': '钱七',
'customer_service_1': '客服小王',
'customer_service_2': '客服小李',
'customer_service_3': '客服小张'
};
if (userMap[safeUserId]) {
return userMap[safeUserId];
}
// 对于manager_开头的ID,显示为客服
if (safeUserId.startsWith('manager_') || safeUserId.includes('manager')) {
return '客服-' + (safeUserId.length >= 4 ? safeUserId.slice(-4) : safeUserId);
}
// 如果都没有找到,返回默认名称,避免出现undefined
if (safeUserId.length >= 4) {
return '用户' + safeUserId.slice(-4);
} else {
return '用户' + safeUserId;
}
} catch (e) {
console.error('获取用户名失败:', e);
return '未知用户';
}
},
/**
* 格式化消息预览
*/
formatMessagePreview: function(message) {
if (message.type === 'system') {
return '[系统消息] ' + message.content;
} else {
const senderPrefix = message.sender === 'me' ? '我: ' : '';
// 限制预览长度
let preview = senderPrefix + message.content;
if (preview.length > 30) {
preview = preview.substring(0, 30) + '...';
}
return preview;
}
},
/**
* 格式化消息时间
*/
formatMessageTime: function(timeStr) {
if (!timeStr) return '';
// 假设时间格式为 "9-21 10:50" 或类似格式
const now = new Date();
let dateStr = timeStr;
// 如果没有包含年份,添加当前年份
if (!timeStr.includes('-') || timeStr.split('-').length < 3) {
const currentYear = now.getFullYear();
dateStr = timeStr.includes(' ') ?
`${currentYear}-${timeStr.split(' ')[0]} ${timeStr.split(' ')[1]}` :
`${currentYear}-${timeStr}`;
}
const messageDate = new Date(dateStr);
const diffTime = Math.abs(now - messageDate);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
// 今天的消息只显示时间
return timeStr.split(' ')[1] || timeStr;
} else if (diffDays === 1) {
// 昨天的消息
return '昨天';
} else if (diffDays < 7) {
// 一周内的消息显示星期
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekdays[messageDate.getDay()];
} else {
// 超过一周的消息显示日期
return timeStr.split(' ')[0] || timeStr;
}
},
/**
* 跳转到聊天详情页
*/
goToChat: function(e) {
const userId = e.currentTarget.dataset.userId;
// 查找对应的用户信息
const userInfo = this.data.messageList.find(item => item.userId === userId);
wx.navigateTo({
url: `/pages/chat-detail/index?userId=${userId}&userName=${encodeURIComponent(userInfo?.userName || '')}`
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function() {
this.loadChatList();
// 停止下拉刷新
wx.stopPullDownRefresh();
},
/**
* 处理清除聊天记录
*/
onUnload: function() {
// 页面卸载时的清理工作
// 恢复原始的全局消息处理函数
try {
const app = getApp();
if (this.originalMessageHandler) {
app.globalData.onNewMessage = this.originalMessageHandler;
} else {
// 如果没有原始处理函数,则清除当前的
delete app.globalData.onNewMessage;
}
console.log('已清理全局新消息处理函数');
} catch (e) {
console.error('清理全局新消息处理函数失败:', e);
}
},
handleClearChat: function(e) {
const userId = e.currentTarget.dataset.userId;
wx.showModal({
title: '确认清空',
content: '确定要清空与该用户的所有聊天记录吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
this.clearChatHistory(userId);
}
}
});
},
/**
* 清空指定用户的聊天记录
*/
clearChatHistory: function(userId) {
try {
wx.removeStorageSync(`chat_messages_${userId}`);
console.log('已清空用户', userId, '的聊天记录');
// 刷新消息列表
this.loadChatList();
wx.showToast({
title: '聊天记录已清空',
icon: 'success'
});
} catch (e) {
console.error('清空聊天记录失败:', e);
wx.showToast({
title: '清空失败',
icon: 'none'
});
}
}
});

4
pages/message-list/index.json

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "消息列表",
"usingComponents": {}
}

30
pages/message-list/index.wxml

@ -0,0 +1,30 @@
<view class="message-list-container">
<view class="page-header">
<text class="page-title">消息列表</text>
</view>
<view class="message-list">
<!-- 消息列表项 -->
<block wx:for="{{messageList}}" wx:key="userId">
<view class="message-item" bindtap="goToChat" data-user-id="{{item.userId}}">
<view class="avatar">
<image src="{{item.avatar || '/images/logo.svg'}}" mode="aspectFit"></image>
</view>
<view class="message-content">
<view class="message-header">
<text class="user-name">{{item.userName}}</text>
<text class="message-time">{{item.lastMessageTime}}</text>
</view>
<view class="message-preview">{{item.lastMessage || '暂无消息'}}</view>
</view>
<view class="message-actions">
<button size="mini" type="warn" bindtap="handleClearChat" data-user-id="{{item.userId}}" catchtap="true">清空</button>
</view>
</view>
</block>
<view wx:if="{{messageList.length === 0}}" class="empty-state">
<text>暂无聊天记录</text>
</view>
</view>
</view>

101
pages/message-list/index.wxss

@ -0,0 +1,101 @@
.message-list-container {
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.page-header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.message-list {
flex: 1;
overflow-y: auto;
}
.message-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
}
.message-item:active {
background-color: #f8f8f8;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
background-color: #f0f0f0;
}
.avatar image {
width: 100%;
height: 100%;
}
.message-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.user-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.message-time {
font-size: 24rpx;
color: #999;
}
.message-preview {
font-size: 28rpx;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-actions {
margin-left: 20rpx;
}
.message-actions button {
font-size: 24rpx;
padding: 0 20rpx;
min-width: 80rpx;
line-height: 50rpx;
}
.empty-state {
padding: 100rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}

38
pages/seller/index.js

@ -2,6 +2,23 @@
const API = require('../../utils/api.js')
Page({
// 分享给朋友/群聊
onShareAppMessage() {
return {
title: '我在鸡蛋贸易平台发布了货源',
path: '/pages/seller/index',
imageUrl: '/images/你有好蛋.png'
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '我在鸡蛋贸易平台发布了货源',
query: '',
imageUrl: '/images/你有好蛋.png'
}
},
data: {
supplies: [],
publishedSupplies: [],
@ -5364,30 +5381,19 @@ Page({
// 联系客服
contactCustomerService() {
wx.showModal({
title: '客服电话',
content: '18140203880',
showCancel: true,
cancelText: '取消',
confirmText: '拨打',
success: (res) => {
if (res.confirm) {
wx.makePhoneCall({
phoneNumber: '18140203880',
wx.navigateTo({
url: '/pages/customer-service/index',
success: () => {
console.log('拨打电话成功');
console.log('跳转到客服页面成功');
},
fail: (err) => {
console.error('拨打电话失败', err);
console.error('跳转到客服页面失败', err);
wx.showToast({
title: '拨打电话失败',
title: '跳转到客服页面失败',
icon: 'none'
});
}
});
}
}
});
},
// 入驻申请

2
pages/seller/index.wxml

@ -92,6 +92,7 @@
<view bindtap="showGoodsDetail" data-item="{{item}}">
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #52c41a; padding: 2rpx 8rpx; border-radius: 10rpx;">已上架</view>
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #52c41a; padding: 2rpx 8rpx; border-radius: 10rpx;">已有{{item.reservedCount || 0}}人想要</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
@ -393,6 +394,7 @@
<view wx:if="{{item.status === 'hidden'}}" style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #8c8c8c; padding: 2rpx 8rpx; border-radius: 10rpx;">已隐藏</view>
<view wx:elif="{{item.status === 'sold_out' || item.status === 'Undercarriage'}}" style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #d9d9d9; padding: 2rpx 8rpx; border-radius: 10rpx;">已下架</view>
<view wx:else style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #999; padding: 2rpx 8rpx; border-radius: 10rpx;">草稿</view>
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #52c41a; padding: 2rpx 8rpx; border-radius: 10rpx;">已有{{item.reservedCount || 0}}人想要</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>

382
pages/test-service/test-service.js

@ -0,0 +1,382 @@
// 客服功能综合测试页面
const socketManager = require('../../utils/websocket');
const API = require('../../utils/api');
Page({
data: {
// 测试状态
testResults: [],
currentTest: '',
isTesting: false,
// 用户信息
userInfo: null,
userType: 'unknown',
isAuthenticated: false,
// WebSocket状态
wsConnected: false,
wsAuthenticated: false,
// 测试消息
testMessage: '测试消息',
receivedMessage: null,
// 测试模式
testMode: 'customer', // customer 或 customer_service
// 模拟客服ID
mockServiceId: 'test_service_001'
},
onLoad: function() {
console.log('测试页面加载');
this.setData({
userInfo: getApp().globalData.userInfo,
userType: getApp().globalData.userType
});
// 设置WebSocket事件监听
this.setupWebSocketListeners();
},
// 设置WebSocket事件监听
setupWebSocketListeners: function() {
// 连接状态监听
socketManager.on('status', (status) => {
console.log('WebSocket状态:', status);
this.setData({
wsConnected: status.type === 'connected'
});
this.addTestResult(`WebSocket状态: ${status.type} - ${status.message || ''}`);
});
// 认证状态监听
socketManager.on('authenticated', (data) => {
console.log('WebSocket认证成功:', data);
this.setData({
wsAuthenticated: true,
isAuthenticated: true
});
this.addTestResult(`WebSocket认证成功,用户类型: ${data.userType || 'unknown'}`);
});
// 消息接收监听
socketManager.on('message', (message) => {
console.log('收到WebSocket消息:', message);
this.setData({
receivedMessage: message
});
this.addTestResult(`收到消息: ${JSON.stringify(message).substring(0, 100)}...`);
});
// 错误监听
socketManager.on('error', (error) => {
console.error('WebSocket错误:', error);
this.addTestResult(`错误: ${error.message || JSON.stringify(error)}`, true);
});
},
// 开始综合测试
startTest: function() {
if (this.data.isTesting) {
wx.showToast({ title: '测试正在进行中', icon: 'none' });
return;
}
this.setData({
isTesting: true,
testResults: [],
currentTest: ''
});
this.addTestResult('===== 开始综合测试 =====');
// 按顺序执行测试用例
setTimeout(() => {
this.testUserTypeDetection();
}, 500);
},
// 测试1: 用户类型检测
testUserTypeDetection: function() {
this.setData({ currentTest: '用户类型检测' });
this.addTestResult('测试1: 验证用户类型检测功能');
const app = getApp();
const userType = app.globalData.userType || 'unknown';
const storedType = wx.getStorageSync('userType') || 'unknown';
this.addTestResult(`全局用户类型: ${userType}`);
this.addTestResult(`本地存储用户类型: ${storedType}`);
if (userType === storedType && userType !== 'unknown') {
this.addTestResult('✓ 用户类型检测通过');
} else {
this.addTestResult('✗ 用户类型检测失败', true);
}
setTimeout(() => {
this.testWebSocketConnection();
}, 1000);
},
// 测试2: WebSocket连接和认证
testWebSocketConnection: function() {
this.setData({ currentTest: 'WebSocket连接' });
this.addTestResult('测试2: 验证WebSocket连接和认证功能');
// 检查是否已连接
if (socketManager.getConnectionStatus()) {
this.addTestResult('✓ WebSocket已连接');
setTimeout(() => {
this.testAuthentication();
}, 500);
} else {
this.addTestResult('尝试建立WebSocket连接...');
// 构建连接URL
const app = getApp();
const userType = app.globalData.userType || 'customer';
const userId = wx.getStorageSync('userId') || `test_${Date.now()}`;
const wsUrl = `ws://localhost:3003?userId=${userId}&userType=${userType}`;
this.addTestResult(`连接URL: ${wsUrl}`);
socketManager.connect(wsUrl);
// 等待连接建立
setTimeout(() => {
if (this.data.wsConnected) {
this.addTestResult('✓ WebSocket连接成功');
this.testAuthentication();
} else {
this.addTestResult('✗ WebSocket连接失败', true);
this.completeTest();
}
}, 3000);
}
},
// 测试3: 认证功能
testAuthentication: function() {
this.setData({ currentTest: '认证功能' });
this.addTestResult('测试3: 验证WebSocket认证功能');
if (this.data.wsAuthenticated) {
this.addTestResult('✓ WebSocket认证成功');
setTimeout(() => {
this.testMessageSending();
}, 500);
} else {
this.addTestResult('尝试手动认证...');
const app = getApp();
const userType = app.globalData.userType || this.data.testMode;
const userId = wx.getStorageSync('userId') || `test_${Date.now()}`;
socketManager.authenticate(userType, userId);
// 等待认证结果
setTimeout(() => {
if (this.data.wsAuthenticated) {
this.addTestResult('✓ 手动认证成功');
this.testMessageSending();
} else {
this.addTestResult('✗ 认证失败', true);
this.completeTest();
}
}, 2000);
}
},
// 测试4: 消息发送功能
testMessageSending: function() {
this.setData({ currentTest: '消息发送' });
this.addTestResult('测试4: 验证消息发送功能');
const app = getApp();
const userType = app.globalData.userType || this.data.testMode;
const targetId = userType === 'customer_service' ?
`test_customer_${Date.now()}` :
this.data.mockServiceId;
this.addTestResult(`当前用户类型: ${userType}, 目标ID: ${targetId}`);
const testMessage = {
type: 'chat_message',
direction: userType === 'customer_service' ? 'service_to_customer' : 'customer_to_service',
data: {
receiverId: targetId,
senderId: wx.getStorageSync('userId') || `test_${Date.now()}`,
senderType: userType,
content: this.data.testMessage || '测试消息_' + Date.now(),
contentType: 1,
timestamp: Date.now()
}
};
const sent = socketManager.send(testMessage);
if (sent) {
this.addTestResult('✓ 消息发送成功');
// 等待接收消息(如果有回复)
setTimeout(() => {
this.testBidirectionalCommunication();
}, 3000);
} else {
this.addTestResult('✗ 消息发送失败', true);
this.completeTest();
}
},
// 测试5: 双向通信功能
testBidirectionalCommunication: function() {
this.setData({ currentTest: '双向通信' });
this.addTestResult('测试5: 验证双向通信功能');
if (this.data.receivedMessage) {
this.addTestResult('✓ 收到响应消息');
this.addTestResult(`消息内容: ${JSON.stringify(this.data.receivedMessage).substring(0, 150)}...`);
} else {
this.addTestResult('⚠ 未收到响应消息(可能是正常的,取决于服务器配置)');
}
setTimeout(() => {
this.testUserTypeSwitching();
}, 1000);
},
// 测试6: 用户类型切换
testUserTypeSwitching: function() {
this.setData({ currentTest: '用户类型切换' });
this.addTestResult('测试6: 验证用户类型切换功能');
try {
// 模拟切换用户类型
const app = getApp();
const originalType = app.globalData.userType;
const newType = originalType === 'customer' ? 'customer_service' : 'customer';
app.globalData.userType = newType;
wx.setStorageSync('userType', newType);
this.addTestResult(`✓ 用户类型切换成功: ${originalType}${newType}`);
this.addTestResult(`新的全局用户类型: ${app.globalData.userType}`);
this.addTestResult(`新的存储用户类型: ${wx.getStorageSync('userType')}`);
// 恢复原始类型
setTimeout(() => {
app.globalData.userType = originalType;
wx.setStorageSync('userType', originalType);
this.addTestResult(`恢复原始用户类型: ${originalType}`);
this.completeTest();
}, 1000);
} catch (error) {
this.addTestResult(`✗ 用户类型切换失败: ${error.message}`, true);
this.completeTest();
}
},
// 完成测试
completeTest: function() {
this.addTestResult('===== 测试完成 =====');
// 统计测试结果
const results = this.data.testResults;
const successCount = results.filter(r => r.includes('✓')).length;
const failCount = results.filter(r => r.includes('✗')).length;
const warnCount = results.filter(r => r.includes('⚠')).length;
this.addTestResult(`测试统计: 成功=${successCount}, 失败=${failCount}, 警告=${warnCount}`);
if (failCount === 0) {
this.addTestResult('🎉 所有测试通过!');
} else {
this.addTestResult('❌ 测试中有失败项,请检查', true);
}
this.setData({
isTesting: false,
currentTest: '测试完成'
});
},
// 添加测试结果
addTestResult: function(message, isError = false) {
const timestamp = new Date().toLocaleTimeString();
const resultItem = {
id: Date.now(),
time: timestamp,
message: message,
isError: isError
};
this.setData({
testResults: [...this.data.testResults, resultItem]
});
console.log(`[${timestamp}] ${message}`);
},
// 切换测试模式
switchTestMode: function() {
const newMode = this.data.testMode === 'customer' ? 'customer_service' : 'customer';
this.setData({ testMode: newMode });
wx.showToast({ title: `已切换到${newMode === 'customer' ? '客户' : '客服'}模式` });
},
// 输入测试消息
onInputChange: function(e) {
this.setData({ testMessage: e.detail.value });
},
// 清理WebSocket连接
cleanup: function() {
try {
socketManager.close();
this.addTestResult('WebSocket连接已关闭');
} catch (error) {
this.addTestResult(`关闭连接失败: ${error.message}`, true);
}
},
// 手动发送消息
sendTestMessage: function() {
if (!this.data.testMessage.trim()) {
wx.showToast({ title: '请输入测试消息', icon: 'none' });
return;
}
const app = getApp();
const userType = app.globalData.userType || this.data.testMode;
const targetId = userType === 'customer_service' ?
`test_customer_${Date.now()}` :
this.data.mockServiceId;
const message = {
type: 'chat_message',
direction: userType === 'customer_service' ? 'service_to_customer' : 'customer_to_service',
data: {
receiverId: targetId,
senderId: wx.getStorageSync('userId') || `test_${Date.now()}`,
senderType: userType,
content: this.data.testMessage.trim(),
contentType: 1,
timestamp: Date.now()
}
};
const sent = socketManager.send(message);
if (sent) {
wx.showToast({ title: '消息已发送' });
this.addTestResult(`手动发送消息: ${this.data.testMessage}`);
} else {
wx.showToast({ title: '发送失败', icon: 'none' });
this.addTestResult(`手动发送失败`, true);
}
},
onUnload: function() {
// 清理监听器和连接
this.cleanup();
}
});

97
pages/test-service/test-service.wxml

@ -0,0 +1,97 @@
<view class="test-container">
<view class="header">
<text class="title">客服功能综合测试</text>
<text class="subtitle">验证客服认证、身份判断和双向沟通功能</text>
</view>
<view class="test-status">
<view class="status-item {{isTesting ? 'testing' : ''}}">
<text class="status-label">当前测试:</text>
<text class="status-value">{{currentTest || '未开始'}}</text>
</view>
<view class="status-item">
<text class="status-label">用户类型:</text>
<text class="status-value">{{userType}}</text>
</view>
<view class="status-item {{wsConnected ? 'connected' : ''}}">
<text class="status-label">WebSocket:</text>
<text class="status-value">{{wsConnected ? '已连接' : '未连接'}}</text>
</view>
<view class="status-item {{wsAuthenticated ? 'authenticated' : ''}}">
<text class="status-label">认证状态:</text>
<text class="status-value">{{wsAuthenticated ? '已认证' : '未认证'}}</text>
</view>
</view>
<view class="test-controls">
<view class="control-item">
<text class="control-label">测试模式:</text>
<view class="control-buttons">
<button type="{{testMode === 'customer' ? 'primary' : 'default'}}" bind:tap="switchTestMode">
{{testMode === 'customer' ? '客户模式 ✓' : '客户模式'}}
</button>
<button type="{{testMode === 'customer_service' ? 'primary' : 'default'}}" bind:tap="switchTestMode">
{{testMode === 'customer_service' ? '客服模式 ✓' : '客服模式'}}
</button>
</view>
</view>
<view class="control-item">
<text class="control-label">测试消息:</text>
<input
class="message-input"
placeholder="请输入测试消息"
value="{{testMessage}}"
bindinput="onInputChange"
/>
<button type="primary" bind:tap="sendTestMessage">发送测试消息</button>
</view>
<view class="action-buttons">
<button
class="start-button"
type="primary"
size="mini"
bind:tap="startTest"
disabled="{{isTesting}}"
>
{{isTesting ? '测试进行中...' : '开始综合测试'}}
</button>
<button
class="cleanup-button"
type="warn"
size="mini"
bind:tap="cleanup"
>
清理连接
</button>
</view>
</view>
<view class="test-results">
<text class="results-title">测试结果:</text>
<scroll-view class="results-list" scroll-y>
<view
wx:for="{{testResults}}"
wx:key="id"
class="result-item {{item.isError ? 'error' : ''}}"
>
<text class="result-time">{{item.time}}</text>
<text class="result-message">{{item.message}}</text>
</view>
<view class="empty-result" wx:if="{{testResults.length === 0}}">
暂无测试结果,点击开始综合测试
</view>
</scroll-view>
</view>
<view class="test-tips">
<text class="tips-title">测试提示:</text>
<view class="tips-content">
<text>1. 测试前请确保已完成登录</text>
<text>2. WebSocket服务需要正常运行</text>
<text>3. 测试将验证用户类型检测、WebSocket连接、认证和消息发送功能</text>
<text>4. 双向通信测试依赖于服务器配置,可能不会收到响应消息</text>
</view>
</view>
</view>

249
pages/test-service/test-service.wxss

@ -0,0 +1,249 @@
.test-container {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 30rpx;
padding: 20rpx 0;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 24rpx;
color: #666;
display: block;
}
.test-status {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.status-item {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-size: 28rpx;
color: #666;
}
.status-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.status-item.testing .status-value {
color: #07c160;
animation: pulse 1s infinite;
}
.status-item.connected .status-value {
color: #07c160;
}
.status-item.authenticated .status-value {
color: #1989fa;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.test-controls {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.control-item {
margin-bottom: 20rpx;
}
.control-item:last-child {
margin-bottom: 0;
}
.control-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 15rpx;
font-weight: 500;
}
.control-buttons {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.control-buttons button {
flex: 1;
font-size: 26rpx;
}
.message-input {
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
margin-bottom: 15rpx;
background-color: #f9f9f9;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 20rpx;
}
.action-buttons button {
flex: 1;
margin: 0 10rpx;
}
.start-button {
background-color: #07c160;
}
.cleanup-button {
background-color: #ee0a24;
}
.test-results {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.results-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: block;
margin-bottom: 15rpx;
}
.results-list {
height: 400rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
padding: 10rpx;
}
.result-item {
padding: 15rpx;
margin-bottom: 10rpx;
background-color: #fff;
border-radius: 6rpx;
border-left: 4rpx solid #1989fa;
font-size: 24rpx;
}
.result-item.error {
border-left-color: #ee0a24;
background-color: #fff1f0;
}
.result-time {
color: #999;
font-size: 20rpx;
display: block;
margin-bottom: 5rpx;
}
.result-message {
color: #333;
word-break: break-all;
line-height: 1.5;
}
.empty-result {
text-align: center;
color: #999;
padding: 60rpx 0;
font-size: 26rpx;
}
.test-tips {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.tips-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: block;
margin-bottom: 15rpx;
}
.tips-content {
background-color: #f0f9ff;
border-radius: 8rpx;
padding: 20rpx;
}
.tips-content text {
display: block;
margin-bottom: 10rpx;
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.tips-content text:last-child {
margin-bottom: 0;
}
/* 适配不同屏幕尺寸 */
@media screen and (min-width: 768px) {
.test-container {
max-width: 900rpx;
margin: 0 auto;
padding: 30rpx;
}
.results-list {
height: 600rpx;
}
}

232
server-example/package-lock.json

@ -16,12 +16,28 @@
"form-data": "^4.0.4",
"multer": "^2.0.2",
"mysql2": "^3.6.5",
"sequelize": "^6.35.2"
"sequelize": "^6.35.2",
"socket.io": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -222,6 +238,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -471,6 +496,19 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/dateformat": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
@ -612,6 +650,67 @@
"node": ">= 0.11.14"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -2012,6 +2111,116 @@
"node": ">=10"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@ -2358,6 +2567,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",

3
server-example/package.json

@ -27,7 +27,8 @@
"form-data": "^4.0.4",
"multer": "^2.0.2",
"mysql2": "^3.6.5",
"sequelize": "^6.35.2"
"sequelize": "^6.35.2",
"socket.io": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.0.1"

125
server-example/query-personnel.js

@ -0,0 +1,125 @@
// 查询userlogin数据库中的personnel表,获取所有采购员数据
require('dotenv').config({ path: 'd:\\xt\\mian_ly\\server-example\\.env' });
const mysql = require('mysql2/promise');
// 查询personnel表中的采购员数据
async function queryPersonnelData() {
let connection = null;
try {
console.log('连接到userlogin数据库...');
connection = await mysql.createConnection({
host: '1.95.162.61', // 直接使用正确的主机地址
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'schl@2025', // 直接使用默认密码
database: 'userlogin',
port: process.env.DB_PORT || 3306,
timezone: '+08:00' // 设置时区为UTC+8
});
console.log('数据库连接成功\n');
// 首先查询personnel表结构
console.log('=== personnel表结构 ===');
const [columns] = await connection.execute(
'SHOW COLUMNS FROM personnel'
);
console.log('字段列表:', columns.map(col => col.Field).join(', '));
console.log();
// 查询projectName为采购员的数据
console.log('=== 所有采购员数据 ===');
const [personnelData] = await connection.execute(
'SELECT * FROM personnel WHERE projectName = ?',
['采购员']
);
console.log(`找到 ${personnelData.length} 名采购员记录`);
console.log('\n采购员数据详情:');
// 格式化输出数据
personnelData.forEach((person, index) => {
console.log(`\n${index + 1}. 采购员信息:`);
console.log(` - ID: ${person.id || 'N/A'}`);
console.log(` - 用户ID: ${person.userId || 'N/A'}`);
console.log(` - 姓名: ${person.name || 'N/A'}`);
console.log(` - 别名: ${person.alias || 'N/A'}`);
console.log(` - 电话号码: ${person.phoneNumber || 'N/A'}`);
console.log(` - 公司: ${person.managercompany || 'N/A'}`);
console.log(` - 部门: ${person.managerdepartment || 'N/A'}`);
console.log(` - 角色: ${person.role || 'N/A'}`);
});
// 输出JSON格式,便于复制使用
console.log('\n=== JSON格式数据 (用于客服页面) ===');
const customerServiceData = personnelData.map((person, index) => ({
id: index + 1,
managerId: person.userId || `PM${String(index + 1).padStart(3, '0')}`,
managercompany: person.managercompany || '未知公司',
managerdepartment: person.managerdepartment || '采购部',
organization: person.organization || '采购组',
projectName: person.role || '采购员',
name: person.name || '未知',
alias: person.alias || person.name || '未知',
phoneNumber: person.phoneNumber || '',
avatarUrl: '',
score: 990 + (index % 10),
isOnline: index % 4 !== 0, // 75%的在线率
responsibleArea: `${getRandomArea()}鸡蛋采购`,
experience: getRandomExperience(),
serviceCount: getRandomNumber(100, 300),
purchaseCount: getRandomNumber(10000, 30000),
profitIncreaseRate: getRandomNumber(10, 25),
profitFarmCount: getRandomNumber(50, 200),
skills: getRandomSkills()
}));
console.log(JSON.stringify(customerServiceData, null, 2));
return customerServiceData;
} catch (error) {
console.error('查询过程中发生错误:', error);
console.error('错误详情:', error.stack);
return null;
} finally {
if (connection) {
await connection.end();
console.log('\n数据库连接已关闭');
}
}
}
// 辅助函数:生成随机区域
function getRandomArea() {
const areas = ['华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'];
return areas[Math.floor(Math.random() * areas.length)];
}
// 辅助函数:生成随机工作经验
function getRandomExperience() {
const experiences = ['1-2年', '1-3年', '2-3年', '3-5年', '5年以上'];
return experiences[Math.floor(Math.random() * experiences.length)];
}
// 辅助函数:生成随机数字
function getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 辅助函数:生成随机技能
function getRandomSkills() {
const allSkills = ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格', '谈判技巧', '库存管理'];
const skillCount = Math.floor(Math.random() * 3) + 2; // 2-4个技能
const selectedSkills = [];
while (selectedSkills.length < skillCount) {
const skill = allSkills[Math.floor(Math.random() * allSkills.length)];
if (!selectedSkills.includes(skill)) {
selectedSkills.push(skill);
}
}
return selectedSkills;
}
// 运行查询
queryPersonnelData();

394
server-example/server-mysql-backup-alias.js

@ -242,6 +242,13 @@ Product.init({
specification: {
type: DataTypes.STRING(255)
},
imageUrls: {
type: DataTypes.TEXT
},
region: {
type: DataTypes.STRING(100),
defaultValue: ''
},
status: {
type: DataTypes.STRING(20),
defaultValue: 'pending_review',
@ -252,6 +259,15 @@ Product.init({
rejectReason: {
type: DataTypes.TEXT
},
// 新增联系人相关字段
product_contact: {
type: DataTypes.STRING(100),
defaultValue: ''
},
contact_phone: {
type: DataTypes.STRING(20),
defaultValue: ''
},
// 新增预约相关字段
reservedCount: {
type: DataTypes.INTEGER,
@ -972,10 +988,9 @@ app.post('/api/user/get', async (req, res) => {
as: 'contacts',
attributes: ['id', 'nickName', 'phoneNumber', 'wechat', 'account', 'accountNumber', 'bank', 'address']
},
{
model: UserManagement,
{ model: UserManagement,
as: 'management',
attributes: ['id', 'managerId', 'company', 'department', 'organization', 'role', 'root']
attributes: ['id', 'managerId', 'managercompany', 'department', 'organization', 'role', 'root']
}
]
});
@ -1070,37 +1085,33 @@ app.post('/api/user/update', async (req, res) => {
// 获取商品列表 - 优化版本确保状态筛选正确应用
app.post('/api/product/list', async (req, res) => {
try {
const { openid, status, keyword, page = 1, pageSize = 20, testMode = false } = req.body;
// 验证openid参数(测试模式除外)
if (!openid && !testMode) {
return res.status(400).json({
success: false,
code: 400,
message: '缺少openid参数'
});
}
const { openid, status, keyword, page = 1, pageSize = 20, testMode = false, viewMode = '' } = req.body;
// 构建查询条件
const where = {};
// 查找用户
let user = null;
if (!testMode) {
if (openid && !testMode) {
user = await User.findOne({ where: { openid } });
// 不再因为用户不存在而返回错误,允许未登录用户访问
if (!user) {
return res.status(404).json({
success: false,
code: 404,
message: '用户不存在'
});
console.log('用户不存在,但仍允许访问公开商品');
}
// 只有管理员可以查看所有商品,普通用户只能查看自己的商品
if (user.type !== 'admin') {
// 如果是卖家查看自己的商品或管理员,应用相应的权限控制
// 但对于购物模式,即使有登录信息也返回所有公开商品
if (user && user.type !== 'admin' && viewMode !== 'shopping') {
where.sellerId = user.userId;
}
} else if (testMode) {
// 测试模式:不验证用户,查询所有已发布的商品
console.log('测试模式:查询所有已发布的商品');
}
// 购物模式:无论用户是否登录,都显示公开商品
if (viewMode === 'shopping' || !openid) {
console.log('购物模式或未登录用户:显示所有公开商品');
}
// 状态筛选 - 直接构建到where对象中,确保不会丢失
@ -1157,44 +1168,43 @@ app.post('/api/product/list', async (req, res) => {
model: User,
as: 'seller',
attributes: ['userId', 'nickName', 'avatarUrl']
},
// 添加CartItem关联以获取预约人数
{
model: CartItem,
as: 'CartItems', // 明确指定别名
attributes: [],
required: false // 允许没有购物车项的商品也能返回
}
],
// 添加selected字段,计算商品被加入购物车的次数(预约人数)
attributes: {
include: [
[Sequelize.fn('COUNT', Sequelize.col('CartItems.id')), 'selected']
]
},
order: [['created_at', 'DESC']],
limit: pageSize,
offset,
// 修复分组问题
group: ['Product.productId', 'seller.userId'] // 使用正确的字段名
// 移除GROUP BY以避免嵌套查询问题
});
// 对于每个商品,单独查询预约人数(cartItems数量)
// 这是为了避免GROUP BY和嵌套查询的复杂问题
const productsWithSelected = await Promise.all(
products.map(async (product) => {
const productJSON = product.toJSON();
const cartItemCount = await CartItem.count({
where: { productId: productJSON.id }
});
productJSON.selected = cartItemCount;
return productJSON;
})
);
// 添加详细日志,记录查询结果
console.log(`商品列表查询结果 - 商品数量: ${count}, 商品列表长度: ${products.length}`);
if (products.length > 0) {
console.log(`第一个商品数据:`, JSON.stringify(products[0], null, 2));
console.log(`商品列表查询结果 - 商品数量: ${count}, 商品列表长度: ${productsWithSelected.length}`);
if (productsWithSelected.length > 0) {
console.log(`第一个商品数据:`, JSON.stringify(productsWithSelected[0], null, 2));
// 添加selected字段的专门日志
console.log('商品预约人数(selected字段)统计:');
products.slice(0, 5).forEach(product => {
const productJSON = product.toJSON();
console.log(`- ${productJSON.productName}: 预约人数=${productJSON.selected || 0}, 商品ID=${productJSON.productId}`);
productsWithSelected.slice(0, 5).forEach(product => {
console.log(`- ${product.productName}: 预约人数=${product.selected || 0}, 商品ID=${product.productId}`);
});
}
// 处理商品列表中的grossWeight字段,确保是数字类型
const processedProducts = products.map(product => {
const productJSON = product.toJSON();
// 处理商品列表中的grossWeight字段,确保是数字类型,同时处理imageUrls的JSON解析
const processedProducts = productsWithSelected.map(product => {
// 创建副本以避免直接修改原始对象
const productJSON = {...product};
// 详细分析毛重字段
const grossWeightDetails = {
@ -1209,21 +1219,77 @@ app.post('/api/product/list', async (req, res) => {
const finalGrossWeight = parseFloat(grossWeightDetails.parsedValue.toFixed(2));
productJSON.grossWeight = finalGrossWeight;
// 确保selected字段存在并设置为数字类型(修复后的代码)
if ('selected' in productJSON) {
// 确保selected是数字类型
// 确保selected字段存在并设置为数字类型
productJSON.selected = productJSON.selected || 0;
productJSON.selected = parseInt(productJSON.selected, 10);
// 修复imageUrls字段:使用简化但可靠的方法确保返回正确格式的URL数组
try {
if (productJSON.imageUrls) {
// 强制转换为字符串
let imageUrlsStr = String(productJSON.imageUrls);
// 方法:直接查找并提取所有有效的URL
// 匹配所有可能的URL模式,避免包含特殊字符
const allUrls = [];
// 查找所有阿里云OSS链接
const ossUrlPattern = /https?:\/\/my-supplier-photos\.oss-cn-chengdu\.aliyuncs\.com[^"\'\[\]\\,]+/g;
const ossUrls = imageUrlsStr.match(ossUrlPattern) || [];
ossUrls.forEach(url => allUrls.push(url));
// 如果没有找到OSS链接,尝试通用URL模式
if (allUrls.length === 0) {
const genericUrlPattern = /https?:\/\/[^"\'\[\]\\,]+/g;
const genericUrls = imageUrlsStr.match(genericUrlPattern) || [];
genericUrls.forEach(url => allUrls.push(url));
}
// 最后的处理:确保返回的是干净的URL数组
productJSON.imageUrls = allUrls.map(url => {
// 彻底清理每个URL
let cleanUrl = url;
// 移除所有可能的末尾特殊字符
while (cleanUrl && (cleanUrl.endsWith('\\') || cleanUrl.endsWith('"') || cleanUrl.endsWith("'"))) {
cleanUrl = cleanUrl.slice(0, -1);
}
return cleanUrl;
}).filter(url => url && url.startsWith('http')); // 过滤掉空值和无效URL
// 如果经过所有处理后仍然没有URL,设置一个默认图片或空数组
if (productJSON.imageUrls.length === 0) {
productJSON.imageUrls = [];
}
// 记录第一个商品的处理信息
if (productsWithSelected.indexOf(product) === 0) {
console.log('商品列表 - imageUrls处理结果:');
console.log('- 原始字符串长度:', imageUrlsStr.length);
console.log('- 提取到的URL数量:', productJSON.imageUrls.length);
console.log('- 最终类型:', Array.isArray(productJSON.imageUrls) ? '数组' : typeof productJSON.imageUrls);
if (productJSON.imageUrls.length > 0) {
console.log('- 第一个URL示例:', productJSON.imageUrls[0]);
console.log('- URL格式验证:', /^https?:\/\/.+$/.test(productJSON.imageUrls[0]) ? '有效' : '无效');
}
}
} else {
// 如果没有selected字段,设置默认值为0
productJSON.selected = 0;
// 如果imageUrls为空,设置为空数组
productJSON.imageUrls = [];
}
} catch (error) {
console.error(`处理商品${productJSON.productId}的imageUrls时出错:`, error);
// 兜底:确保始终返回数组
productJSON.imageUrls = [];
}
// 记录第一个商品的转换信息用于调试
if (products.indexOf(product) === 0) {
if (productsWithSelected.indexOf(product) === 0) {
console.log('商品列表 - 第一个商品毛重字段处理:');
console.log('- 原始值:', grossWeightDetails.value, '类型:', grossWeightDetails.type);
console.log('- 转换后的值:', finalGrossWeight, '类型:', typeof finalGrossWeight);
console.log('- selected字段: 存在=', 'selected' in productJSON, '值=', productJSON.selected, '类型=', typeof productJSON.selected);
console.log('- selected字段: 值=', productJSON.selected, '类型:', typeof productJSON.selected);
}
return productJSON;
@ -1645,6 +1711,93 @@ app.post('/api/products/delete', async (req, res) => {
}
});
// 更新商品联系人信息API
app.post('/api/products/update-contacts', async (req, res) => {
try {
// 查找需要更新联系人信息的商品(已发布但联系人信息为空)
const products = await Product.findAll({
where: {
status: 'published',
[Sequelize.Op.or]: [
{ product_contact: null },
{ product_contact: '' },
{ contact_phone: null },
{ contact_phone: '' }
]
},
attributes: ['productId']
});
if (products.length === 0) {
return res.json({
success: true,
code: 200,
message: '没有需要更新联系人信息的商品'
});
}
// 查找所有用户,用于随机分配联系人
const users = await User.findAll({
where: {
[Sequelize.Op.or]: [
{ role: 'seller' },
{ role: '管理员' }
],
[Sequelize.Op.and]: [
{ phoneNumber: { [Sequelize.Op.not]: null } },
{ phoneNumber: { [Sequelize.Op.not]: '' } },
{ nickName: { [Sequelize.Op.not]: null } },
{ nickName: { [Sequelize.Op.not]: '' } }
]
},
attributes: ['nickName', 'phoneNumber']
});
if (users.length === 0) {
return res.json({
success: false,
code: 500,
message: '没有可用的卖家用户来分配联系人信息'
});
}
// 随机为每个商品分配联系人信息
let updatedCount = 0;
for (const product of products) {
// 随机选择一个用户
const randomUser = users[Math.floor(Math.random() * users.length)];
// 更新商品联系人信息
await Product.update(
{
product_contact: randomUser.nickName,
contact_phone: randomUser.phoneNumber,
updated_at: new Date()
},
{
where: { productId: product.productId }
}
);
updatedCount++;
}
res.json({
success: true,
code: 200,
message: `商品联系人信息更新成功,共更新了${updatedCount}个商品`
});
} catch (error) {
console.error('更新商品联系人信息失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '更新商品联系人信息失败',
error: error.message
});
}
});
// 添加商品到购物车
app.post('/api/cart/add', async (req, res) => {
// 增加全局错误捕获,确保即使在try-catch外部的错误也能被处理
@ -2958,6 +3111,141 @@ app.post('/api/product/edit', async (req, res) => {
}
});
// 提交入驻申请
app.post('/api/settlement/submit', async (req, res) => {
try {
const { openid,
collaborationid,
cooperation,
company,
phoneNumber,
province,
city,
district,
businesslicenseurl,
proofurl,
brandurl
} = req.body;
console.log('收到入驻申请:', req.body);
// 验证必填字段
if (!openid || !collaborationid || !cooperation || !company || !phoneNumber || !province || !city || !district) {
return res.status(400).json({
success: false,
code: 400,
message: '请填写完整的申请信息'
});
}
// 查找用户信息
const user = await User.findOne({ where: { openid } });
if (!user) {
return res.status(404).json({
success: false,
code: 404,
message: '用户不存在'
});
}
// 检查用户是否已有入驻信息且状态为审核中
if (user.collaborationid && user.partnerstatus === 'underreview') {
return res.status(400).json({
success: false,
code: 400,
message: '您已有待审核的入驻申请,请勿重复提交'
});
}
// 更新用户表中的入驻信息
// 转换collaborationid为中文(使用明确的英文标识以避免混淆)
let collaborationidCN = collaborationid;
if (collaborationid === 'chicken') {
collaborationidCN = '鸡场';
} else if (collaborationid === 'trader') {
collaborationidCN = '贸易商';
}
// 兼容旧的wholesale标识
else if (collaborationid === 'wholesale') {
collaborationidCN = '贸易商';
}
// 转换cooperation为中文合作模式(使用明确的英文标识以避免混淆)
// 直接使用传入的中文合作模式,确保支持:资源委托、自主定义销售、区域包场合作、其他
let cooperationCN = cooperation;
// 如果传入的是英文值,则进行映射
if (cooperation === 'resource_delegation') {
cooperationCN = '资源委托';
} else if (cooperation === 'self_define_sales') {
cooperationCN = '自主定义销售';
} else if (cooperation === 'regional_exclusive') {
cooperationCN = '区域包场合作';
} else if (cooperation === 'other') {
cooperationCN = '其他';
}
// 兼容旧的wholesale标识
else if (cooperation === 'wholesale') {
cooperationCN = '资源委托';
}
// 兼容旧的self_define标识
else if (cooperation === 'self_define') {
cooperationCN = '自主定义销售';
}
// 确保存储的是中文合作模式
// 执行更新操作
const updateResult = await User.update({
collaborationid: collaborationidCN, // 合作商身份(中文)
cooperation: cooperationCN, // 合作模式(中文)
company: company, // 公司名称
phoneNumber: phoneNumber, // 电话号码
province: province, // 省份
city: city, // 城市
district: district, // 区县
businesslicenseurl: businesslicenseurl || '', // 营业执照 - NOT NULL约束,使用空字符串
proofurl: proofurl || '', // 证明材料 - NOT NULL约束,使用空字符串
brandurl: brandurl || '', // 品牌授权链文件
partnerstatus: 'underreview', // 合作商状态明确设置为审核中,覆盖数据库默认值
updated_at: new Date()
}, {
where: { userId: user.userId }
});
// 验证更新是否成功
const updatedUser = await User.findOne({ where: { userId: user.userId } });
console.log('更新后的用户状态:', updatedUser.partnerstatus);
// 双重确认:如果状态仍不是underreview,再次更新
if (updatedUser && updatedUser.partnerstatus !== 'underreview') {
console.warn('检测到状态未更新正确,执行二次更新:', updatedUser.partnerstatus);
await User.update({
partnerstatus: 'underreview'
}, {
where: { userId: user.userId }
});
}
console.log('用户入驻信息更新成功,用户ID:', user.userId);
res.json({
success: true,
code: 200,
message: '入驻申请提交成功,请等待审核',
data: {
status: 'pending'
}
});
} catch (error) {
console.error('提交入驻申请失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '提交入驻申请失败: ' + error.message
});
}
});
// 导出模型和Express应用供其他模块使用
module.exports = {
User,

960
server-example/server-mysql.js

@ -7,6 +7,7 @@ const multer = require('multer');
const path = require('path');
const fs = require('fs');
const OssUploader = require('./oss-uploader');
const WebSocket = require('ws');
require('dotenv').config();
// 创建Express应用
@ -17,6 +18,16 @@ const PORT = process.env.PORT || 3003;
const http = require('http');
const server = http.createServer(app);
// 创建WebSocket服务器
const wss = new WebSocket.Server({ server });
// 连接管理器 - 存储所有活跃的WebSocket连接
const connections = new Map();
// 用户在线状态管理器
const onlineUsers = new Map(); // 存储用户ID到连接的映射
const onlineManagers = new Map(); // 存储客服ID到连接的映射
// 配置连接管理
server.maxConnections = 20; // 增加最大连接数限制
@ -5744,6 +5755,949 @@ app.post('/api/products/update-contacts', async (req, res) => {
}
});
// REST API接口 - 获取用户会话列表
app.get('/api/conversations/user/:userId', async (req, res) => {
try {
const userId = req.params.userId;
const conversations = await getUserConversations(userId);
res.status(200).json({
success: true,
code: 200,
data: conversations
});
} catch (error) {
console.error('获取用户会话列表失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取会话列表失败: ' + error.message
});
}
});
// REST API接口 - 获取客服会话列表
app.get('/api/conversations/manager/:managerId', async (req, res) => {
try {
const managerId = req.params.managerId;
const conversations = await getManagerConversations(managerId);
res.status(200).json({
success: true,
code: 200,
data: conversations
});
} catch (error) {
console.error('获取客服会话列表失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取会话列表失败: ' + error.message
});
}
});
// REST API接口 - 获取会话历史消息
app.get('/api/conversations/:conversationId/messages', async (req, res) => {
try {
const conversationId = req.params.conversationId;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 50;
const offset = (page - 1) * limit;
const [messages] = await sequelize.query(
`SELECT * FROM chat_messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?`,
{ replacements: [conversationId, limit, offset] }
);
// 反转顺序,使最早的消息在前
messages.reverse();
// 获取消息总数
const [[totalCount]] = await sequelize.query(
'SELECT COUNT(*) as count FROM chat_messages WHERE conversation_id = ?',
{ replacements: [conversationId] }
);
res.status(200).json({
success: true,
code: 200,
data: {
messages,
pagination: {
page,
limit,
total: totalCount.count,
totalPages: Math.ceil(totalCount.count / limit)
}
}
});
} catch (error) {
console.error('获取历史消息失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取历史消息失败: ' + error.message
});
}
});
// REST API接口 - 标记消息已读
app.post('/api/conversations/:conversationId/read', async (req, res) => {
try {
const conversationId = req.params.conversationId;
const { userId, managerId, type } = req.body;
const now = new Date();
let updateField;
if (type === 'user') {
// 用户标记客服消息为已读
updateField = 'unread_count';
await sequelize.query(
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 2',
{ replacements: [now, conversationId] }
);
} else if (type === 'manager') {
// 客服标记用户消息为已读
updateField = 'cs_unread_count';
await sequelize.query(
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 1',
{ replacements: [now, conversationId] }
);
} else {
throw new Error('无效的类型');
}
// 重置未读计数
await sequelize.query(
`UPDATE chat_conversations SET ${updateField} = 0 WHERE conversation_id = ?`,
{ replacements: [conversationId] }
);
res.status(200).json({
success: true,
code: 200,
message: '已标记为已读'
});
} catch (error) {
console.error('标记已读失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '标记已读失败: ' + error.message
});
}
});
// REST API接口 - 获取在线统计信息
app.get('/api/online-stats', async (req, res) => {
try {
const stats = getOnlineStats();
res.status(200).json({
success: true,
code: 200,
data: stats
});
} catch (error) {
console.error('获取在线统计失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取在线统计失败: ' + error.message
});
}
});
// REST API接口 - 获取客服列表 - 修改为按照用户需求:工位名为采购员且有电话号码
app.get('/api/managers', async (req, res) => {
try {
// 查询userlogin数据库中的personnel表,获取工位为采购员且有电话号码的用户
// 根据表结构选择所有需要的字段
const [personnelData] = await sequelize.query(
'SELECT id, managerId, managercompany, managerdepartment, organization, projectName, name, alias, phoneNumber, avatarUrl FROM userlogin.personnel WHERE projectName = ? AND phoneNumber IS NOT NULL AND phoneNumber != "" ORDER BY id ASC',
{ replacements: ['采购员'] }
);
// 将获取的数据映射为前端需要的格式,添加online状态(从onlineManagers Map中判断)
// 防御性编程确保onlineManagers存在且正确使用
const isManagerOnline = (id, managerId) => {
// 确保onlineManagers存在且是Map类型
if (!onlineManagers || typeof onlineManagers.has !== 'function') {
return false;
}
// 尝试多种可能的键类型
return onlineManagers.has(id) || onlineManagers.has(String(id)) || (managerId && onlineManagers.has(managerId));
};
const managers = personnelData.map((person, index) => ({
id: person.id,
managerId: person.managerId || `PM${String(index + 1).padStart(3, '0')}`,
managercompany: person.managercompany || '未知公司',
managerdepartment: person.managerdepartment || '采购部',
organization: person.organization || '采购组',
projectName: person.projectName || '采购员',
name: person.name || '未知',
alias: person.alias || person.name || '未知',
phoneNumber: person.phoneNumber || '',
avatar: person.avatarUrl || '', // 使用表中的avatarUrl字段
online: isManagerOnline(person.id, person.managerId) // 安全地检查在线状态,传递both id and managerId
}));
res.status(200).json({
success: true,
code: 200,
data: managers
});
} catch (error) {
console.error('获取客服列表失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取客服列表失败: ' + error.message
});
}
});
// WebSocket服务器事件处理
wss.on('connection', (ws, req) => {
console.log('新的WebSocket连接建立');
// 生成连接ID
const connectionId = crypto.randomUUID();
ws.connectionId = connectionId;
// 存储连接信息
connections.set(connectionId, {
ws,
userId: null,
managerId: null,
isUser: false,
isManager: false,
connectedAt: new Date()
});
// 连接认证处理
ws.on('message', async (message) => {
try {
// 更新连接活动时间
updateConnectionActivity(ws.connectionId);
const data = JSON.parse(message.toString());
// 处理认证消息
if (data.type === 'auth' || data.action === 'auth') {
await handleAuth(ws, data);
return;
}
// 处理心跳消息
if (data.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
// 处理聊天消息
if (data.type === 'chat_message') {
const payload = data.data || data.payload || data;
await handleChatMessage(ws, payload);
return;
}
// 处理会话相关消息
if (data.type === 'session') {
// 直接传递整个data对象给handleSessionMessage,因为action可能在data根级别
await handleSessionMessage(ws, data);
return;
}
// 处理未读消息标记
if (data.type === 'mark_read') {
const payload = data.data || data.payload || data;
await handleMarkRead(ws, payload);
return;
}
} catch (error) {
console.error('处理WebSocket消息错误:', error);
ws.send(JSON.stringify({
type: 'error',
message: '消息处理失败'
}));
}
});
// 连接关闭处理
ws.on('close', () => {
console.log('WebSocket连接关闭');
const connection = connections.get(connectionId);
if (connection) {
// 更新在线状态
if (connection.isUser && connection.userId) {
onlineUsers.delete(connection.userId);
updateUserOnlineStatus(connection.userId, 0);
} else if (connection.isManager && connection.managerId) {
onlineManagers.delete(connection.managerId);
updateManagerOnlineStatus(connection.managerId, 0);
}
// 从连接池中移除
connections.delete(connectionId);
}
});
// 连接错误处理
ws.on('error', (error) => {
console.error('WebSocket连接错误:', error);
});
});
// 认证处理函数
async function handleAuth(ws, data) {
// 兼容不同格式的认证数据
const payload = data.data || data;
const { userId, managerId, type } = payload;
const connection = connections.get(ws.connectionId);
if (!connection) {
ws.send(JSON.stringify({
type: 'auth_error',
message: '连接已断开'
}));
return;
}
// 验证用户或客服身份
if (type === 'user' && userId) {
connection.userId = userId;
connection.isUser = true;
connection.userType = 'user'; // 添加userType字段确保与其他函数兼容性
onlineUsers.set(userId, ws);
await updateUserOnlineStatus(userId, 1);
// 发送认证成功消息
ws.send(JSON.stringify({
type: 'auth_success',
payload: { userId, type: 'user' }
}));
console.log(`用户 ${userId} 已连接`);
} else if (type === 'manager' && managerId) {
connection.managerId = managerId;
connection.isManager = true;
connection.userType = 'manager'; // 添加userType字段确保与其他函数兼容性
onlineManagers.set(managerId, ws);
await updateManagerOnlineStatus(managerId, 1);
// 发送认证成功消息
ws.send(JSON.stringify({
type: 'auth_success',
payload: { managerId, type: 'manager' }
}));
console.log(`客服 ${managerId} 已连接`);
} else {
// 无效的认证信息
ws.send(JSON.stringify({
type: 'auth_error',
message: '无效的认证信息'
}));
}
}
// 更新用户在线状态
async function updateUserOnlineStatus(userId, status) {
try {
// 更新chat_conversations表中用户的在线状态
await sequelize.query(
'UPDATE chat_conversations SET user_online = ? WHERE userId = ?',
{ replacements: [status, userId] }
);
// 通知相关客服用户状态变化
const conversations = await sequelize.query(
'SELECT DISTINCT managerId FROM chat_conversations WHERE userId = ?',
{ replacements: [userId] }
);
conversations[0].forEach(conv => {
const managerWs = onlineManagers.get(conv.managerId);
if (managerWs) {
managerWs.send(JSON.stringify({
type: 'user_status_change',
payload: { userId, online: status === 1 }
}));
}
});
} catch (error) {
console.error('更新用户在线状态失败:', error);
}
}
// 更新客服在线状态
async function updateManagerOnlineStatus(managerId, status) {
try {
// 更新chat_conversations表中客服的在线状态
await sequelize.query(
'UPDATE chat_conversations SET cs_online = ? WHERE managerId = ?',
{ replacements: [status, managerId] }
);
// 同步更新客服表中的在线状态 - 注释掉因为online字段不存在
// await sequelize.query(
// 'UPDATE userlogin.personnel SET online = ? WHERE id = ?',
// { replacements: [status, managerId] }
// );
// 通知相关用户客服状态变化
const conversations = await sequelize.query(
'SELECT DISTINCT userId FROM chat_conversations WHERE managerId = ?',
{ replacements: [managerId] }
);
conversations[0].forEach(conv => {
const userWs = onlineUsers.get(conv.userId);
if (userWs) {
userWs.send(JSON.stringify({
type: 'manager_status_change',
payload: { managerId, online: status === 1 }
}));
}
});
// 通知其他客服状态变化
onlineManagers.forEach((ws, id) => {
if (id !== managerId) {
ws.send(JSON.stringify({
type: 'manager_status_change',
payload: { managerId, online: status === 1 }
}));
}
});
} catch (error) {
console.error('更新客服在线状态失败:', error);
}
}
// 添加定时检查连接状态的函数
function startConnectionMonitoring() {
// 每30秒检查一次连接状态
setInterval(async () => {
try {
// 检查所有连接的活跃状态
const now = Date.now();
connections.forEach((connection, connectionId) => {
const { ws, lastActive = now } = connection;
// 如果超过60秒没有活动,关闭连接
if (now - lastActive > 60000) {
console.log(`关闭超时连接: ${connectionId}`);
try {
ws.close();
} catch (e) {
console.error('关闭连接失败:', e);
}
}
});
// 发送广播心跳给所有在线用户和客服
onlineUsers.forEach(ws => {
try {
ws.send(JSON.stringify({ type: 'heartbeat' }));
} catch (e) {
console.error('发送心跳失败给用户:', e);
}
});
onlineManagers.forEach(ws => {
try {
ws.send(JSON.stringify({ type: 'heartbeat' }));
} catch (e) {
console.error('发送心跳失败给客服:', e);
}
});
} catch (error) {
console.error('连接监控错误:', error);
}
}, 30000);
}
// 更新连接的最后活动时间
function updateConnectionActivity(connectionId) {
const connection = connections.get(connectionId);
if (connection) {
connection.lastActive = Date.now();
}
}
// 获取在线统计信息
function getOnlineStats() {
return {
totalConnections: connections.size,
onlineUsers: onlineUsers.size,
onlineManagers: onlineManagers.size,
activeConnections: Array.from(connections.entries()).filter(([_, conn]) => {
return (conn.isUser && conn.userId) || (conn.isManager && conn.managerId);
}).length
};
}
// 会话管理函数
// 创建或获取现有会话
async function createOrGetConversation(userId, managerId) {
try {
// 尝试查找已存在的会话
const [existingConversations] = await sequelize.query(
'SELECT * FROM chat_conversations WHERE userId = ? AND managerId = ? LIMIT 1',
{ replacements: [userId, managerId] }
);
if (existingConversations && existingConversations.length > 0) {
const conversation = existingConversations[0];
// 如果会话已结束,重新激活
if (conversation.status !== 1) {
await sequelize.query(
'UPDATE chat_conversations SET status = 1 WHERE conversation_id = ?',
{ replacements: [conversation.conversation_id] }
);
conversation.status = 1;
}
return conversation;
}
// 创建新会话
const conversationId = crypto.randomUUID();
const now = new Date();
await sequelize.query(
`INSERT INTO chat_conversations
(conversation_id, userId, managerId, status, user_online, cs_online, created_at, updated_at)
VALUES (?, ?, ?, 1, ?, ?, ?, ?)`,
{
replacements: [
conversationId,
userId,
managerId,
onlineUsers.has(userId) ? 1 : 0,
onlineManagers.has(managerId) ? 1 : 0,
now,
now
]
}
);
// 返回新创建的会话
return {
conversation_id: conversationId,
userId,
managerId,
status: 1,
user_online: onlineUsers.has(userId) ? 1 : 0,
cs_online: onlineManagers.has(managerId) ? 1 : 0,
created_at: now,
updated_at: now
};
} catch (error) {
console.error('创建或获取会话失败:', error);
throw error;
}
}
// 获取用户的所有会话
async function getUserConversations(userId) {
try {
const [conversations] = await sequelize.query(
`SELECT c.*, u.nickName as userNickName, u.avatarUrl as userAvatar,
p.name as managerName
FROM chat_conversations c
LEFT JOIN users u ON c.userId = u.userId
LEFT JOIN userlogin.personnel p ON c.managerId = p.id
WHERE c.userId = ?
ORDER BY c.last_message_time DESC, c.created_at DESC`,
{ replacements: [userId] }
);
return conversations;
} catch (error) {
console.error('获取用户会话失败:', error);
throw error;
}
}
// 获取客服的所有会话
async function getManagerConversations(managerId) {
try {
const [conversations] = await sequelize.query(
`SELECT c.*, u.nickName as userNickName, u.avatarUrl as userAvatar,
p.name as managerName
FROM chat_conversations c
LEFT JOIN users u ON c.userId = u.userId
LEFT JOIN userlogin.personnel p ON c.managerId = p.id
WHERE c.managerId = ?
ORDER BY c.last_message_time DESC, c.created_at DESC`,
{ replacements: [managerId] }
);
return conversations;
} catch (error) {
console.error('获取客服会话失败:', error);
throw error;
}
}
// 消息处理函数
// 处理聊天消息
async function handleChatMessage(ws, payload) {
const { conversationId, content, contentType = 1, fileUrl, fileSize, duration } = payload;
const connection = connections.get(ws.connectionId);
if (!connection) {
ws.send(JSON.stringify({
type: 'error',
message: '连接已失效'
}));
return;
}
try {
// 确定发送者和接收者信息
let senderId, receiverId, senderType;
let conversation;
if (connection.isUser) {
// 用户发送消息给客服
senderId = connection.userId;
senderType = 1;
// 如果没有提供会话ID,则查找或创建会话
if (!conversationId) {
if (!payload.managerId) {
throw new Error('未指定客服ID');
}
receiverId = payload.managerId;
conversation = await createOrGetConversation(senderId, receiverId);
} else {
// 获取会话信息以确定接收者
const [conversations] = await sequelize.query(
'SELECT * FROM chat_conversations WHERE conversation_id = ?',
{ replacements: [conversationId] }
);
if (!conversations || conversations.length === 0) {
throw new Error('会话不存在');
}
conversation = conversations[0];
receiverId = conversation.managerId;
}
} else if (connection.isManager) {
// 客服发送消息给用户
senderId = connection.managerId;
senderType = 2;
// 获取会话信息以确定接收者
const [conversations] = await sequelize.query(
'SELECT * FROM chat_conversations WHERE conversation_id = ?',
{ replacements: [conversationId] }
);
if (!conversations || conversations.length === 0) {
throw new Error('会话不存在');
}
conversation = conversations[0];
receiverId = conversation.userId;
} else {
throw new Error('未认证的连接');
}
// 生成消息ID和时间戳
const messageId = crypto.randomUUID();
const now = new Date();
// 存储消息
await storeMessage({
messageId,
conversationId: conversation.conversation_id,
senderType,
senderId,
receiverId,
contentType,
content,
fileUrl,
fileSize,
duration,
createdAt: now
});
// 更新会话最后消息
await updateConversationLastMessage(conversation.conversation_id, content, now);
// 更新未读计数
if (connection.isUser) {
await updateUnreadCount(conversation.conversation_id, 'cs_unread_count', 1);
} else {
await updateUnreadCount(conversation.conversation_id, 'unread_count', 1);
}
// 构造消息对象
const messageData = {
messageId,
conversationId: conversation.conversation_id,
senderType,
senderId,
receiverId,
contentType,
content,
fileUrl,
fileSize,
duration,
isRead: 0,
status: 1,
createdAt: now
};
// 发送消息给接收者
let receiverWs;
if (senderType === 1) {
// 用户发送给客服
receiverWs = onlineManagers.get(receiverId);
} else {
// 客服发送给用户
receiverWs = onlineUsers.get(receiverId);
}
// 处理特殊情况:当发送者和接收者是同一个人(既是用户又是客服)
// 检查是否存在另一个身份的连接
if (!receiverWs && senderId == receiverId) {
if (senderType === 1) {
// 用户发送消息给自己的客服身份
receiverWs = onlineManagers.get(senderId);
} else {
// 客服发送消息给自己的用户身份
receiverWs = onlineUsers.get(senderId);
}
}
if (receiverWs) {
receiverWs.send(JSON.stringify({
type: 'new_message',
payload: messageData
}));
}
// 发送确认给发送者
ws.send(JSON.stringify({
type: 'message_sent',
payload: {
messageId,
status: 'success'
}
}));
} catch (error) {
console.error('处理聊天消息失败:', error);
ws.send(JSON.stringify({
type: 'error',
message: '消息发送失败: ' + error.message
}));
}
}
// 存储消息到数据库
async function storeMessage(messageData) {
const { messageId, conversationId, senderType, senderId, receiverId,
contentType, content, fileUrl, fileSize, duration, createdAt } = messageData;
try {
await sequelize.query(
`INSERT INTO chat_messages
(message_id, conversation_id, sender_type, sender_id, receiver_id,
content_type, content, file_url, file_size, duration, is_read, status,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?)`,
{
replacements: [
messageId, conversationId, senderType, senderId, receiverId,
contentType, content, fileUrl || null, fileSize || null, duration || null,
createdAt, createdAt
]
}
);
} catch (error) {
console.error('存储消息失败:', error);
throw error;
}
}
// 更新会话最后消息
async function updateConversationLastMessage(conversationId, lastMessage, timestamp) {
try {
await sequelize.query(
'UPDATE chat_conversations SET last_message = ?, last_message_time = ?, updated_at = ? WHERE conversation_id = ?',
{ replacements: [lastMessage, timestamp, timestamp, conversationId] }
);
} catch (error) {
console.error('更新会话最后消息失败:', error);
throw error;
}
}
// 更新未读计数
async function updateUnreadCount(conversationId, countField, increment) {
try {
await sequelize.query(
`UPDATE chat_conversations
SET ${countField} = ${countField} + ?, updated_at = ?
WHERE conversation_id = ?`,
{ replacements: [increment, new Date(), conversationId] }
);
} catch (error) {
console.error('更新未读计数失败:', error);
throw error;
}
}
// 处理未读消息标记
async function handleMarkRead(ws, payload) {
const { conversationId, messageIds } = payload;
const connection = connections.get(ws.connectionId);
if (!connection) return;
try {
const now = new Date();
let countField;
if (connection.isUser) {
// 用户标记客服消息为已读
countField = 'unread_count';
await sequelize.query(
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 2',
{ replacements: [now, conversationId] }
);
} else if (connection.isManager) {
// 客服标记用户消息为已读
countField = 'cs_unread_count';
await sequelize.query(
'UPDATE chat_messages SET is_read = 1, read_time = ? WHERE conversation_id = ? AND sender_type = 1',
{ replacements: [now, conversationId] }
);
}
// 重置未读计数
await sequelize.query(
`UPDATE chat_conversations SET ${countField} = 0 WHERE conversation_id = ?`,
{ replacements: [conversationId] }
);
// 发送确认
ws.send(JSON.stringify({
type: 'marked_read',
payload: { conversationId }
}));
} catch (error) {
console.error('标记消息已读失败:', error);
ws.send(JSON.stringify({
type: 'error',
message: '标记已读失败'
}));
}
}
// 处理会话相关消息
async function handleSessionMessage(ws, data) {
// 兼容不同格式的消息数据
const action = data.action || (data.data && data.data.action) || (data.payload && data.payload.action) || 'list'; // 默认action为'list'
const conversationId = data.conversationId || (data.data && data.data.conversationId) || (data.payload && data.payload.conversationId);
const connection = connections.get(ws.connectionId);
if (!connection) {
ws.send(JSON.stringify({
type: 'error',
message: '未认证的连接'
}));
return;
}
try {
switch (action) {
case 'get_conversations':
case 'list':
// 获取会话列表,支持'list'和'get_conversations'两种操作
let conversations;
if (connection.isUser || connection.userType === 'user') {
const userId = connection.userId || connection.userType === 'user' && connection.userId;
conversations = await getUserConversations(userId);
} else if (connection.isManager || connection.userType === 'manager') {
const managerId = connection.managerId || connection.userType === 'manager' && connection.managerId;
conversations = await getManagerConversations(managerId);
}
// 支持两种响应格式,确保兼容性
if (action === 'list') {
// 兼容测试脚本的响应格式
ws.send(JSON.stringify({
type: 'session_list',
data: conversations
}));
} else {
// 原有响应格式
ws.send(JSON.stringify({
type: 'conversations_list',
payload: { conversations }
}));
}
break;
case 'get_messages':
// 获取会话历史消息
if (!conversationId) {
throw new Error('未指定会话ID');
}
const [messages] = await sequelize.query(
`SELECT * FROM chat_messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT 50`,
{ replacements: [conversationId] }
);
// 反转顺序,使最早的消息在前
messages.reverse();
ws.send(JSON.stringify({
type: 'messages_list',
payload: { messages, conversationId }
}));
break;
case 'close_conversation':
// 关闭会话
if (!conversationId) {
throw new Error('未指定会话ID');
}
const status = connection.isUser ? 3 : 2;
await sequelize.query(
'UPDATE chat_conversations SET status = ? WHERE conversation_id = ?',
{ replacements: [status, conversationId] }
);
ws.send(JSON.stringify({
type: 'conversation_closed',
payload: { conversationId }
}));
break;
}
} catch (error) {
console.error('处理会话消息失败:', error);
ws.send(JSON.stringify({
type: 'error',
message: error.message
}));
}
}
// 在服务器启动前执行商品联系人更新
updateProductContacts().then(() => {
console.log('\n📦 商品联系人信息更新完成!');
@ -5753,11 +6707,17 @@ updateProductContacts().then(() => {
// 无论更新成功与否,都启动服务器
// 启动服务器监听 - 使用配置好的http server对象
// 监听0.0.0.0以允许通过所有网络接口访问(包括IPv4地址)
// 启动连接监控
startConnectionMonitoring();
console.log('连接监控服务已启动');
server.listen(PORT, '0.0.0.0', () => {
console.log(`\n🚀 服务器启动成功,监听端口 ${PORT}`);
console.log(`API 服务地址: http://localhost:${PORT}`);
console.log(`API 通过IP访问地址: http://192.168.0.98:${PORT}`);
console.log(`WebSocket 服务地址: ws://localhost:${PORT}`);
console.log(`服务器最大连接数限制: ${server.maxConnections}`);
console.log(`WebSocket 服务器已启动,等待连接...`);
});
});

79
server-example/test-managers-api.js

@ -0,0 +1,79 @@
// 测试修复后的/api/managers接口
const http = require('http');
console.log('开始测试/api/managers接口...');
const options = {
hostname: 'localhost',
port: 3003,
path: '/api/managers',
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(`状态码: ${res.statusCode}`);
try {
const responseData = JSON.parse(data);
console.log('响应数据:', JSON.stringify(responseData, null, 2));
// 验证响应格式
if (res.statusCode === 200) {
if (responseData.success === true || responseData.code === 200) {
console.log('✅ API接口返回成功状态');
// 检查数据字段
const dataArray = responseData.data || responseData;
if (Array.isArray(dataArray)) {
console.log(`✅ 获取到 ${dataArray.length} 条客服数据`);
// 检查数据结构
if (dataArray.length > 0) {
const firstItem = dataArray[0];
console.log('第一条数据结构:', Object.keys(firstItem));
// 检查必要字段
const requiredFields = ['id', 'managerId', 'name', 'phoneNumber'];
const missingFields = requiredFields.filter(field => !(field in firstItem));
if (missingFields.length === 0) {
console.log('✅ 所有必要字段都存在');
} else {
console.warn('⚠️ 缺少必要字段:', missingFields);
}
}
console.log('🎉 测试通过!API接口正常工作');
} else {
console.error('❌ 响应数据不是预期的数组格式');
}
} else {
console.error('❌ API返回非成功状态:', responseData);
}
} else {
console.error(`❌ 请求失败,状态码: ${res.statusCode}`);
}
} catch (parseError) {
console.error('❌ JSON解析错误:', parseError.message);
console.error('原始响应数据:', data);
}
});
});
req.on('error', (e) => {
console.error('❌ 请求错误:', e.message);
});
req.end();
console.log('测试脚本已启动,请等待测试结果...');

138
simple_chat_test.js

@ -0,0 +1,138 @@
// 简化版聊天功能测试
// 服务器配置
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();
}

333
test-customer-service.js

@ -0,0 +1,333 @@
// 客服功能测试脚本
// 用于验证客服认证、身份判断和双向沟通功能
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
};

96
test_chat_connection.js

@ -0,0 +1,96 @@
// 测试聊天功能连接的脚本
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();

1276
test_chat_functionality.js

File diff suppressed because it is too large

75
update_product_table.js

@ -0,0 +1,75 @@
// 更新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();

110
utils/api.js

@ -9,6 +9,21 @@ const SERVER_CONFIG = {
DEFAULT_LOCAL_IP: 'http://192.168.1.100:3003' // 默认本地IP地址
};
// 强制清理可能导致端口错误的本地存储
function cleanupTestModeStorage() {
try {
wx.removeStorageSync('__TEST_MODE__');
wx.removeStorageSync('__TEST_SERVER_IP__');
wx.removeStorageSync('__DEVICE_TYPE__');
console.log('已清理可能导致端口错误的本地存储配置');
} catch (e) {
console.warn('清理存储时出错:', e);
}
}
// 立即执行清理
cleanupTestModeStorage();
// 重要提示:真机调试时,请确保以下操作:
// 1. 将手机和开发机连接到同一WiFi网络
// 2. 修改上方DEFAULT_LOCAL_IP为您开发机的实际IP地址
@ -1297,6 +1312,7 @@ module.exports = {
console.log('商品数量:', data.products.length);
// 增强处理:确保每个商品都包含正确的selected字段和有效的图片URL
// 同时处理前后端字段映射:productId -> id, productName -> name
const processedProducts = data.products.map(product => {
// 优先使用product.selected,其次使用其他可能的字段
// 这确保了即使服务器返回的数据格式不一致,前端也能正确显示预约人数
@ -1309,16 +1325,24 @@ module.exports = {
String(product.productId) === 'product_1760080711896_9gb6u2tig') {
console.log('===== 特定商品信息 =====');
console.log('原始商品ID:', product.id, 'productId:', product.productId);
console.log('原始商品名称:', product.name, 'productName:', product.productName);
console.log('原始selected字段值:', product.selected);
console.log('原始reservedCount字段值:', product.reservedCount);
console.log('原始reservationCount字段值:', product.reservationCount);
console.log('处理后的selectedCount值:', selectedCount);
}
// 返回处理后的商品数据,确保包含selected字段和原始图片URL
// 返回处理后的商品数据,确保包含selected字段、正确的ID名称字段映射,以及原始图片URL
return {
...product,
selected: selectedCount // 确保selected字段存在
// 确保id字段存在,优先使用productId,其次使用id
id: product.productId || product.id,
// 确保name字段存在,优先使用productName,其次使用name
name: product.productName || product.name,
// 确保selected字段存在
selected: selectedCount,
// 确保displayGrossWeight字段存在(如果前端需要)
displayGrossWeight: product.grossWeight ? `${product.grossWeight}` : '0'
};
});
@ -1445,9 +1469,46 @@ module.exports = {
wx.setStorageSync('userId', phoneRes.data.userId);
userId = phoneRes.data.userId;
}
// 获取用户信息以判断是否为客服
this.getUserInfo(openid).then(userInfoRes => {
console.log('获取用户信息成功:', userInfoRes);
let userInfo = null;
let userType = 'customer'; // 默认客户类型
// 处理不同格式的响应
if (userInfoRes && userInfoRes.data) {
userInfo = userInfoRes.data;
// 判断用户类型 - 支持多种格式
if (userInfo.userType === 'customer_service' ||
userInfo.type === 'customer_service' ||
userInfo.isService === true ||
userInfo.isManager === true) {
userType = 'customer_service';
}
}
// 存储用户类型信息
wx.setStorageSync('userType', userType);
// 更新全局用户信息
if (getApp && getApp().globalData) {
getApp().globalData.userInfo = userInfo;
getApp().globalData.userType = userType;
}
resolve({
success: true,
data: { openid, userId, sessionKey, phoneRes, userInfo, userType }
});
}).catch(userInfoErr => {
console.warn('获取用户信息失败(不影响登录):', userInfoErr);
// 如果获取用户信息失败,仍然返回登录成功,但用户类型默认为客户
wx.setStorageSync('userType', 'customer');
resolve({
success: true,
data: { openid, userId, sessionKey, phoneRes }
data: { openid, userId, sessionKey, phoneRes, userType: 'customer' }
});
});
}).catch(phoneErr => {
console.error('手机号上传失败:', phoneErr);
@ -1459,9 +1520,45 @@ module.exports = {
});
} else {
// 没有手机号信息,直接返回登录成功
// 获取用户信息以判断是否为客服
this.getUserInfo(openid).then(userInfoRes => {
console.log('获取用户信息成功:', userInfoRes);
let userInfo = null;
let userType = 'customer'; // 默认客户类型
// 处理不同格式的响应
if (userInfoRes && userInfoRes.data) {
userInfo = userInfoRes.data;
// 判断用户类型 - 支持多种格式
if (userInfo.userType === 'customer_service' ||
userInfo.type === 'customer_service' ||
userInfo.isService === true ||
userInfo.isManager === true) {
userType = 'customer_service';
}
}
// 存储用户类型信息
wx.setStorageSync('userType', userType);
// 更新全局用户信息
if (getApp && getApp().globalData) {
getApp().globalData.userInfo = userInfo;
getApp().globalData.userType = userType;
}
resolve({
success: true,
data: { openid, userId, sessionKey, userInfo, userType }
});
}).catch(userInfoErr => {
console.warn('获取用户信息失败(不影响登录):', userInfoErr);
// 如果获取用户信息失败,仍然返回登录成功,但用户类型默认为客户
wx.setStorageSync('userType', 'customer');
resolve({
success: true,
data: { openid, userId, sessionKey }
data: { openid, userId, sessionKey, userType: 'customer' }
});
});
}
} else {
@ -2047,6 +2144,11 @@ module.exports = {
return request('/api/products/update-contacts', 'POST');
},
// 预约商品
reserveProduct: function ({ id }) {
return request('/api/products/reserve', 'POST', { productId: id });
},
/**
* 上传入驻申请文件
* @param {String} filePath - 本地文件路径

492
utils/websocket.js

@ -0,0 +1,492 @@
// utils/websocket.js
// WebSocket连接管理器
class WebSocketManager {
constructor() {
this.socket = null;
this.url = '';
this.isConnected = false;
this.isAuthenticated = false; // 新增:认证状态标记
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000; // 3秒后重连
this.heartbeatInterval = null;
this.heartbeatTime = 30000; // 30秒心跳
this.messageQueue = []; // 未发送的消息队列
this.listeners = {}; // 事件监听器
this.lastHeartbeatTime = 0; // 最后一次心跳响应时间
this.isManualDisconnect = false; // 是否手动断开连接
// 清理可能导致端口错误的存储配置
this._cleanupStorage();
}
// 清理可能导致端口错误的存储配置
_cleanupStorage() {
try {
// 尝试在小程序环境中清理存储
if (typeof wx !== 'undefined' && wx.removeStorageSync) {
wx.removeStorageSync('__TEST_MODE__');
wx.removeStorageSync('__TEST_SERVER_IP__');
wx.removeStorageSync('__DEVICE_TYPE__');
console.log('WebSocket: 已清理可能导致端口错误的本地存储配置');
}
} catch (e) {
console.warn('WebSocket: 清理存储时出错:', e);
}
}
/**
* 初始化WebSocket连接
* @param {string} url - WebSocket服务器地址
* @param {object} options - 配置选项
*/
connect(url, options = {}) {
if (this.socket && this.isConnected) {
console.log('WebSocket已经连接');
return;
}
this.url = url;
this.maxReconnectAttempts = options.maxReconnectAttempts || this.maxReconnectAttempts;
this.reconnectInterval = options.reconnectInterval || this.reconnectInterval;
this.heartbeatTime = options.heartbeatTime || this.heartbeatTime;
this.isManualDisconnect = false; // 重置手动断开标志
try {
console.log('尝试连接WebSocket:', url);
this._trigger('status', { type: 'connecting', message: '正在连接服务器...' });
this.socket = wx.connectSocket({
url: url,
success: () => {
console.log('WebSocket连接请求已发送');
},
fail: (error) => {
console.error('WebSocket连接请求失败:', error);
this._trigger('error', error);
this._trigger('status', { type: 'error', message: '连接服务器失败' });
this._reconnect();
}
});
this._setupEventHandlers();
} catch (error) {
console.error('WebSocket初始化失败:', error);
this._trigger('error', error);
this._trigger('status', { type: 'error', message: '连接异常' });
this._reconnect();
}
}
/**
* 设置WebSocket事件处理器
*/
_setupEventHandlers() {
if (!this.socket) return;
// 连接成功
this.socket.onOpen(() => {
console.log('WebSocket连接已打开');
this.isConnected = true;
this.isAuthenticated = false; // 重置认证状态
this.reconnectAttempts = 0;
this.lastHeartbeatTime = Date.now(); // 记录最后心跳时间
this._trigger('open');
this._trigger('status', { type: 'connected', message: '连接成功' });
// 连接成功后立即进行认证
this.authenticate();
this._startHeartbeat();
});
// 接收消息
this.socket.onMessage((res) => {
try {
let data = JSON.parse(res.data);
// 处理心跳响应
if (data.type === 'pong') {
this.lastHeartbeatTime = Date.now(); // 更新心跳时间
return;
}
// 处理认证响应
if (data.type === 'auth_response') {
if (data.success) {
console.log('WebSocket认证成功');
this.isAuthenticated = true;
// 触发认证成功事件,并传递用户类型信息
this._trigger('authenticated', { userType: data.userType || 'customer' });
// 认证成功后发送队列中的消息
this._flushMessageQueue();
} else {
console.error('WebSocket认证失败:', data.message);
this.isAuthenticated = false;
this._trigger('authFailed', { message: data.message, userType: data.userType || 'unknown' });
}
return;
}
// 处理客服状态更新消息
if (data.type === 'customerServiceStatusUpdate') {
console.log('处理客服状态更新:', data);
this._trigger('customerServiceStatusUpdate', data);
return;
}
console.log('接收到消息:', data);
this._trigger('message', data);
} catch (error) {
console.error('消息解析失败:', error);
this._trigger('error', error);
}
});
// 连接关闭
this.socket.onClose((res) => {
console.log('WebSocket连接已关闭:', res);
this.isConnected = false;
this._stopHeartbeat();
this._trigger('close', res);
this._trigger('status', { type: 'disconnected', message: '连接已关闭' });
// 尝试重连
if (res.code !== 1000 && !this.isManualDisconnect) { // 非正常关闭且不是手动断开
this._reconnect();
}
});
// 连接错误
this.socket.onError((error) => {
console.error('WebSocket错误:', error);
this._trigger('error', error);
this._trigger('status', { type: 'error', message: '连接发生错误' });
});
}
/**
* 发送认证消息
* @param {string} userType - 用户类型'user''customerService'
* @param {string} userId - 用户ID
*/
authenticate(userType = null, userId = null) {
try {
// 获取登录用户信息或token
const app = getApp();
const globalUserInfo = app.globalData.userInfo || {};
// 如果传入了参数,优先使用传入的参数
const finalUserType = userType || globalUserInfo.userType || globalUserInfo.type || 'customer';
const finalUserId = userId || globalUserInfo.userId || wx.getStorageSync('userId') || `temp_${Date.now()}`;
console.log('发送WebSocket认证消息:', { userId: finalUserId, userType: finalUserType });
// 构建认证消息
const authMessage = {
type: 'auth',
timestamp: Date.now(),
data: {
userId: finalUserId,
userType: finalUserType,
// 可以根据实际需求添加更多认证信息
}
};
// 直接发送认证消息,不经过常规消息队列
if (this.isConnected && this.socket) {
this.socket.send({
data: JSON.stringify(authMessage),
success: () => {
console.log('认证消息发送成功');
},
fail: (error) => {
console.error('认证消息发送失败:', error);
// 认证失败后尝试重新认证
setTimeout(() => {
this.authenticate(userType, userId);
}, 2000);
}
});
}
} catch (error) {
console.error('发送认证消息异常:', error);
}
}
/**
* 发送消息
* @param {object} data - 要发送的数据
* @returns {boolean} 消息是否已成功放入发送队列不保证实际发送成功
*/
send(data) {
// 验证消息格式
if (!data || typeof data !== 'object') {
console.error('WebSocket发送消息失败: 消息格式不正确');
return false;
}
// 为消息添加时间戳
if (!data.timestamp) {
data.timestamp = Date.now();
}
// 如果是认证消息或连接未建立,直接处理
if (data.type === 'auth' || data.type === 'ping') {
// 认证消息和心跳消息不需要等待认证
if (this.isConnected && this.socket) {
try {
this.socket.send({
data: JSON.stringify(data),
success: () => {
console.log('特殊消息发送成功:', data);
this._trigger('sendSuccess', data);
},
fail: (error) => {
console.error('特殊消息发送失败:', error);
this._trigger('sendError', error);
}
});
return true;
} catch (error) {
console.error('发送特殊消息异常:', error);
this._trigger('error', error);
return false;
}
}
} else if (this.isConnected && this.socket) {
// 非特殊消息需要检查认证状态
if (!this.isAuthenticated) {
console.log('WebSocket未认证,消息已加入队列等待认证');
this.messageQueue.push(data);
// 如果未认证,尝试重新认证
if (!this.isAuthenticated) {
this.authenticate();
}
return true;
}
try {
this.socket.send({
data: JSON.stringify(data),
success: () => {
console.log('消息发送成功:', data);
this._trigger('sendSuccess', data);
},
fail: (error) => {
console.error('消息发送失败:', error);
// 将失败的消息加入队列
this.messageQueue.push(data);
this._trigger('sendError', error);
}
});
return true;
} catch (error) {
console.error('发送消息异常:', error);
this.messageQueue.push(data);
this._trigger('error', error);
return false;
}
} else {
// 连接未建立,加入消息队列
console.log('WebSocket未连接,消息已加入队列');
this.messageQueue.push(data);
// 尝试重连
if (!this.isConnected) {
this._reconnect();
}
return true;
}
}
/**
* 关闭WebSocket连接
*/
close() {
if (this.socket) {
this._stopHeartbeat();
this.isManualDisconnect = true; // 标记为手动断开
this.socket.close();
this.socket = null;
this.isConnected = false;
this.isAuthenticated = false; // 重置认证状态
console.log('WebSocket已主动关闭');
this._trigger('status', { type: 'disconnected', message: '连接已断开' });
}
}
/**
* 开始心跳检测
*/
_startHeartbeat() {
this._stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
// 检查是否超过3倍心跳间隔未收到心跳响应
if (Date.now() - this.lastHeartbeatTime > this.heartbeatTime * 3) {
console.warn('WebSocket心跳超时,可能已断开连接');
this._stopHeartbeat();
this._reconnect();
return;
}
if (this.isConnected) {
this.send({ type: 'ping', timestamp: Date.now() });
console.log('发送心跳包');
}
}, this.heartbeatTime);
}
/**
* 停止心跳检测
*/
_stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* 尝试重新连接
*/
_reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('WebSocket重连次数已达上限,停止重连');
this._trigger('reconnectFailed');
this._trigger('status', {
type: 'error',
isWarning: true,
message: `已达到最大重连次数(${this.maxReconnectAttempts}次)`
});
return;
}
this.reconnectAttempts++;
// 增加重连时间间隔(指数退避)
const currentInterval = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1);
console.log(`WebSocket第${this.reconnectAttempts}次重连... 间隔: ${currentInterval}ms`);
this._trigger('reconnecting', this.reconnectAttempts);
this._trigger('status', {
type: 'reconnecting',
message: `正在重连(${this.reconnectAttempts}/${this.maxReconnectAttempts})`
});
setTimeout(() => {
this.connect(this.url);
}, currentInterval);
}
/**
* 发送队列中的消息
*/
_flushMessageQueue() {
if (this.messageQueue.length > 0) {
console.log('发送队列中的消息,队列长度:', this.messageQueue.length);
// 循环发送队列中的消息,使用小延迟避免消息发送过快
const sendMessage = () => {
if (this.messageQueue.length === 0 || !this.isConnected) {
return;
}
const messageData = this.messageQueue.shift();
const message = JSON.stringify(messageData);
this.socket.send({
data: message,
success: () => {
console.log('队列消息发送成功:', messageData);
// 继续发送下一条消息,添加小延迟
setTimeout(sendMessage, 50);
},
fail: (error) => {
console.error('队列消息发送失败:', error);
// 发送失败,重新加入队列
this.messageQueue.unshift(messageData);
}
});
};
// 开始发送队列中的第一条消息
sendMessage();
}
}
/**
* 触发事件
* @param {string} event - 事件名称
* @param {*} data - 事件数据
*/
_trigger(event, data = null) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`事件处理错误 [${event}]:`, error);
}
});
}
}
/**
* 监听事件
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数
*/
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
/**
* 移除事件监听
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数不传则移除所有该事件的监听器
*/
off(event, callback) {
if (!this.listeners[event]) return;
if (callback) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
} else {
this.listeners[event] = [];
}
}
/**
* 获取连接状态
* @returns {boolean} 是否连接
*/
getConnectionStatus() {
return this.isConnected;
}
/**
* 获取认证状态
* @returns {boolean} 是否已认证
*/
getAuthStatus() {
return this.isAuthenticated;
}
/**
* 获取重连次数
* @returns {number} 重连次数
*/
getReconnectAttempts() {
return this.reconnectAttempts;
}
/**
* 清空消息队列
*/
clearMessageQueue() {
this.messageQueue = [];
}
}
// 导出单例
export default new WebSocketManager();
Loading…
Cancel
Save