fix(phase2a): 代码审查修复 - 8项关键/重要问题
安全修复: - WebSocket Token 增加 Redis 有效性校验(已登出用户无法建立 WS) 功能修复: - GetRecommendFriends 改为批量查询,正确返回用户名/昵称/头像 - 上下线通知:OnlineService 通过接口注入获取好友列表推送状态变更 - 管理端在线用户 API 补充用户名信息 代码质量: - 所有 json.Marshal/Redis 错误增加检查与日志 - ContactController 13 个 endpoint 统一走 handleError 业务错误映射 - 管理端 Controller 补全包注释、函数注释和结构化日志 - 前端 5 个联系人页面 avatar 工具函数抽取到 utils/avatar.js Made-with: Cursor
This commit is contained in:
@@ -53,23 +53,12 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
const loading = ref(true)
|
||||
const processingMap = reactive({})
|
||||
|
||||
const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#4F46E5']
|
||||
|
||||
const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const handleUnblock = (user) => {
|
||||
if (processingMap[user.user_id]) return
|
||||
uni.showModal({
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
const loading = ref(true)
|
||||
@@ -73,18 +74,6 @@ const friend = ref(null)
|
||||
const userId = ref(0)
|
||||
const processing = ref(false)
|
||||
|
||||
const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#4F46E5']
|
||||
|
||||
const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const currentGroupName = computed(() => {
|
||||
if (!friend.value || !friend.value.group_id) return '默认分组'
|
||||
const group = contactStore.groups.find(g => g.id === friend.value.group_id)
|
||||
@@ -128,7 +117,7 @@ const editRemark = () => {
|
||||
}
|
||||
|
||||
const selectGroup = () => {
|
||||
const groups = [{ id: 0, name: '默认分组' }, ...contactStore.groups]
|
||||
const groups = [{ id: null, name: '默认分组' }, ...contactStore.groups]
|
||||
const names = groups.map(g => g.name)
|
||||
|
||||
uni.showActionSheet({
|
||||
|
||||
@@ -116,6 +116,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { useWebSocketStore } from '@/store/websocket'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
import CustomTabBar from '@/components/CustomTabBar.vue'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
@@ -125,19 +126,6 @@ const userStore = useUserStore()
|
||||
const searchKeyword = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#7C3AED', '#4F46E5']
|
||||
|
||||
const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
const code = name.charCodeAt(0)
|
||||
return AVATAR_COLORS[code % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const filteredFriends = computed(() => {
|
||||
if (!searchKeyword.value) return contactStore.friendList
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
|
||||
@@ -68,23 +68,12 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
const loading = ref(true)
|
||||
const processingMap = reactive({})
|
||||
|
||||
const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#4F46E5']
|
||||
|
||||
const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const formatTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import contactApi from '@/api/contact'
|
||||
import { useContactStore } from '@/store/contact'
|
||||
import { getAvatarColor, getInitial } from '@/utils/avatar'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
|
||||
@@ -121,18 +122,6 @@ const recommendLoading = ref(true)
|
||||
const recommendations = ref([])
|
||||
const addingMap = reactive({})
|
||||
|
||||
const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#4F46E5']
|
||||
|
||||
const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const doSearch = async () => {
|
||||
const kw = keyword.value.trim()
|
||||
if (!kw) {
|
||||
@@ -143,7 +132,7 @@ const doSearch = async () => {
|
||||
searchLoading.value = true
|
||||
try {
|
||||
const res = await contactApi.searchUsers(kw)
|
||||
searchResults.value = res.data || []
|
||||
searchResults.value = res.data?.list || []
|
||||
} catch (e) {
|
||||
console.error('搜索用户失败', e)
|
||||
searchResults.value = []
|
||||
|
||||
@@ -89,10 +89,18 @@ class WebSocketService {
|
||||
* @returns {number} 消息序号
|
||||
*/
|
||||
send(event, data = {}) {
|
||||
// #ifdef H5
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[WS] 连接未就绪,无法发送')
|
||||
return -1
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
if (!this.ws) {
|
||||
console.warn('[WS] 连接未就绪,无法发送')
|
||||
return -1
|
||||
}
|
||||
// #endif
|
||||
|
||||
this.seq++
|
||||
const msg = JSON.stringify({
|
||||
|
||||
@@ -144,8 +144,14 @@ export const useContactStore = defineStore('contact', () => {
|
||||
if (friend) friend.is_online = online
|
||||
}
|
||||
|
||||
/** 初始化 WebSocket 事件监听 */
|
||||
/** 防止 WebSocket 事件监听重复注册 */
|
||||
let _wsInitialized = false
|
||||
|
||||
/** 初始化 WebSocket 事件监听(幂等,多次调用只注册一次) */
|
||||
const initWsListeners = () => {
|
||||
if (_wsInitialized) return
|
||||
_wsInitialized = true
|
||||
|
||||
wsService.on('notify.friend.request', () => {
|
||||
fetchPendingRequests()
|
||||
})
|
||||
|
||||
@@ -20,17 +20,23 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
||||
/** 是否已连接 */
|
||||
const isConnected = computed(() => connected.value)
|
||||
|
||||
/** 防止 _connected/_disconnected 监听器重复注册 */
|
||||
let _initialized = false
|
||||
|
||||
/**
|
||||
* 初始化 WebSocket 连接
|
||||
* @param {string} [token] - JWT Token
|
||||
*/
|
||||
const connect = (token) => {
|
||||
wsService.on('_connected', () => {
|
||||
connected.value = true
|
||||
})
|
||||
wsService.on('_disconnected', () => {
|
||||
connected.value = false
|
||||
})
|
||||
if (!_initialized) {
|
||||
wsService.on('_connected', () => {
|
||||
connected.value = true
|
||||
})
|
||||
wsService.on('_disconnected', () => {
|
||||
connected.value = false
|
||||
})
|
||||
_initialized = true
|
||||
}
|
||||
wsService.connect(token)
|
||||
}
|
||||
|
||||
|
||||
21
frontend/src/utils/avatar.js
Normal file
21
frontend/src/utils/avatar.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 头像工具函数
|
||||
*
|
||||
* 提供头像颜色生成和首字母提取功能,供联系人相关页面统一使用。
|
||||
* 颜色基于用户名首字符 charCode 做确定性映射,保证同一用户每次展示颜色一致。
|
||||
*/
|
||||
|
||||
export const AVATAR_COLORS = ['#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#7C3AED', '#4F46E5']
|
||||
|
||||
/** 根据用户名生成确定性头像背景色 */
|
||||
export const getAvatarColor = (name) => {
|
||||
if (!name) return AVATAR_COLORS[0]
|
||||
const code = name.charCodeAt(0)
|
||||
return AVATAR_COLORS[code % AVATAR_COLORS.length]
|
||||
}
|
||||
|
||||
/** 提取用户名首字母(大写) */
|
||||
export const getInitial = (name) => {
|
||||
if (!name) return '?'
|
||||
return name.charAt(0).toUpperCase()
|
||||
}
|
||||
Reference in New Issue
Block a user