使用cursor来开发UniApp + SQL Server 客服聊天功能


1. 功能概述


本功能基于UniApp + SQL Server实现了一个类似微信的即时聊天系统,支持用户和客服进行实时对话。主要特点:



在线演示

演示地址: http://demo.xiyueta.com/case/web20250222/

测试账号: demo

测试密码: 123456


使用效果


用户端聊天


客服端管理


会话列表


我的


2. 数据库设计


2.1 聊天消息表

CREATE TABLE pre20250222_chat (
    message_id INT IDENTITY(1,1) PRIMARY KEY,  -- 消息ID 
    sender_type TINYINT NOT NULL,              -- 发送者类型(1:用户 2:客服)
    sender_id INT NOT NULL,                    -- 发送者ID
    accept_id INT NOT NULL,                    -- 接收者ID
    content NVARCHAR(MAX) NOT NULL,            -- 消息内容
    is_read TINYINT DEFAULT 0,                 -- 是否已读(0:未读 1:已读)
    create_time DATETIME DEFAULT GETDATE()     -- 发送时间
)

-- 创建索引
CREATE INDEX idx_sender ON pre20250222_chat (sender_id, sender_type)
CREATE INDEX idx_accept ON pre20250222_chat (accept_id)
CREATE INDEX idx_create_time ON pre20250222_chat (create_time)

2.2 字段说明


3. 核心设计思路


3.1 消息加载机制

1. 首次加载:


2. 历史消息加载:


3. 新消息检查:


4. 发送消息:


3.2 已读状态处理

1. 客服查看时:


2. 用户查看时:


4. 前端实现


4.1 技术选型


4.2 核心代码与问题解决


4.2.1 消息列表组件

<template>
  <scroll-view 
    class="message-list" 
    scroll-y
    :scroll-anchoring="true"  <!-- 解决iOS滚动抖动 -->
    :refresher-enabled="true"
    :refresher-triggered="refreshing"
    @refresherrefresh="loadMoreMessages"
    @scrolltolower="onScrollToLower"
    :scroll-into-view="scrollIntoView"
    ref="messageListRef"
  >
    <!-- 加载提示 -->
    <view class="load-more">
      <text v-if="loading">加载中...</text>
      <text v-else-if="!hasMore">没有更多消息了</text>
      <text v-else>下拉加载更多</text>
    </view>
    
    <!-- 消息列表 -->
    <view 
      v-for="(message, index) in messages"
      :key="message.message_id" 
      :id="'msg-' + message.message_id"
      class="message-item"
      :class="getMessageClass(message)"
    >
      <!-- 日期分割线 -->
      <view class="date-divider" v-if="showDateDivider(index)">
        {{ formatDate(message.create_time) }}
      </view>

      <!-- 消息内容 -->
      <view class="content">
        <view class="bubble">{{ message.content }}</view>
        <view class="time">{{ formatTime(message.create_time) }}</view>
      </view>
    </view>
  </scroll-view>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import dayjs from 'dayjs'

// 状态管理
const messages = ref([])
const loading = ref(false)
const refreshing = ref(false)
const hasMore = ref(true)
const scrollIntoView = ref('')
const latestMessageId = ref(0)
const earliestMessageId = ref(999999)

// 定时器
let refreshTimer = null

// 格式化时间
const formatTime = (time) => {
  return dayjs(time).format('HH:mm')
}

// 格式化日期
const formatDate = (time) => {
  const date = dayjs(time)
  const today = dayjs()
  
  if(date.isSame(today, 'day')) {
    return '今天'
  } else if(date.isSame(today.subtract(1, 'day'), 'day')) {
    return '昨天'  
  }
  return date.format('MM-DD')
}

// 显示日期分割线
const showDateDivider = (index) => {
  if(index === 0) return true
  
  const curr = dayjs(messages.value[index].create_time)
  const prev = dayjs(messages.value[index - 1].create_time)
  return !curr.isSame(prev, 'day')
}

// 获取消息样式
const getMessageClass = computed(() => (message) => {
  return {
    'self': message.sender_id === userInfo.value.user_id,
    'other': message.sender_id !== userInfo.value.user_id,
    'unread': !message.is_read
  }
})

// 获取消息列表
const getMessages = async () => {
  if (loading.value) return
  
  loading.value = true
  try {
    const params = {
      pageSize: 10,
      chat_with_id: chatWithId.value
    }
    
    // 加载历史消息
    if (refreshing.value) {
      params.before_id = earliestMessageId.value
    }
    // 获取新消息
    else {
      params.after_id = latestMessageId.value
    }

    const res = await api.chat.list(params)
    if(res.status === 'y') {
      const { list, latest_id, earliest_id } = res.data
      
      // 更新消息ID范围
      if(latest_id) latestMessageId.value = latest_id
      if(earliest_id) earliestMessageId.value = earliest_id
      
      // 更新消息列表
      if(refreshing.value) {
        messages.value = [...list, ...messages.value]
      } else {
        messages.value = [...messages.value, ...list]
        // 新消息滚动到底部
        scrollIntoView.value = `msg-${latest_id}`
      }
      
      hasMore.value = list.length === 10
    }
  } finally {
    loading.value = false
    refreshing.value = false
  }
}

// 定时刷新
const startRefreshTimer = () => {
  refreshTimer = setInterval(async () => {
    await getMessages()
  }, 30000)
}

onMounted(() => {
  getMessages()
  startRefreshTimer()
})

onUnmounted(() => {
  if(refreshTimer) clearInterval(refreshTimer)
})

// 页面显示时刷新
onShow(() => {
  getMessages()
})
</script>

<style lang="scss" scoped>
.message-list {
  height: calc(100vh - 100rpx); // 减去输入框高度
  background: #f5f5f5;
  
  .date-divider {
    text-align: center;
    font-size: 24rpx;
    color: #999;
    margin: 20rpx 0;
  }
  
  .message-item {
    margin: 20rpx;
    
    &.self {
      .content {
        flex-direction: row-reverse;
      }
      .bubble {
        background: #95EC69;
      }
    }
    
    .content {
      display: flex;
      align-items: flex-start;
    }
    
    .bubble {
      max-width: 60%;
      padding: 20rpx;
      border-radius: 8rpx;
      background: #fff;
      word-break: break-all;
    }
    
    .time {
      font-size: 24rpx;
      color: #999;
      margin: 0 20rpx;
    }
  }
}
</style>

4.2.2 常见问题及解决方案


1. iOS滚动抖动问题


2. 消息时间分组显示


3. 新消息提醒


4. 图片消息预览


5. 后端实现


5.1 核心功能设计


5.1.1 消息排序策略

1. 历史消息查询


2. 新消息查询


5.1.2 已读状态更新


5.2 核心SQL实现


' 获取消息列表
dim sql: sql = "SELECT c.*,"
sql = sql & "u1.username as sender_name,"
sql = sql & "u2.username as accept_name "
sql = sql & "FROM " & db_PREFIX & "chat c "
sql = sql & "LEFT JOIN " & db_PREFIX & "user u1 ON c.sender_id = u1.user_id "
sql = sql & "LEFT JOIN " & db_PREFIX & "user u2 ON c.accept_id = u2.user_id "
sql = sql & "WHERE (c.sender_id=" & chatWithId & " AND c.accept_id=" & userId & ") "
sql = sql & "OR (c.sender_id=" & userId & " AND c.accept_id=" & chatWithId & ") "

' 根据查询类型构建条件
if beforeId <> "" then
  ' 获取历史消息 - 降序排序
  sql = sql & "AND c.message_id < " & beforeId & " "
  sql = sql & "ORDER BY c.message_id DESC "
elseif afterId <> "" then
  ' 获取新消息 - 升序排序
  sql = sql & "AND c.message_id > " & afterId & " "
  sql = sql & "ORDER BY c.message_id ASC "
else
  ' 首次加载 - 降序排序后反转
  sql = sql & "ORDER BY c.message_id DESC "
end if

' 分页
if beforeId = "" and afterId = "" then
  sql = sql & "OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY"
end if

' 更新已读状态
dim updateSql: updateSql = "UPDATE " & db_PREFIX & "chat SET is_read=1 "
updateSql = updateSql & "WHERE accept_id=" & userId & " "
if userRs("role") = 1 then
  ' 客服查看 - 更新用户发送的消息
  updateSql = updateSql & "AND sender_type=1 AND sender_id=" & chatWithId
else
  ' 用户查看 - 更新客服发送的消息  
  updateSql = updateSql & "AND sender_type=2"
end if
conn.Execute(updateSql)

' 获取最新消息ID
dim maxSql: maxSql = "SELECT MAX(message_id) as max_id FROM " & db_PREFIX & "chat "
maxSql = maxSql & "WHERE (sender_id=" & chatWithId & " AND accept_id=" & userId & ") "
maxSql = maxSql & "OR (sender_id=" & userId & " AND accept_id=" & chatWithId & ")"

' 获取最早消息ID  
dim minSql: minSql = "SELECT MIN(message_id) as min_id FROM " & db_PREFIX & "chat "
minSql = minSql & "WHERE (sender_id=" & chatWithId & " AND accept_id=" & userId & ") "
minSql = minSql & "OR (sender_id=" & userId & " AND accept_id=" & chatWithId & ")"

5.3 SQL优化说明


1. 联表查询


2. 索引使用


3. 分页优化


4. 批量更新


6. 总结


本项目基于UniApp + SQL Server打造的即时聊天系统,实现了实时对话、历史消息、已读状态等核心功能,提供了流畅的用户体验和稳定的性能表现。


关于作者

如果本文章对您有所帮助,欢迎交流和探讨技术问题。

QQ: 313801120

更多文章: www.xiyueta.com/

希望能一起成长,共同探索更多开发技巧!