使用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/

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