This commit is contained in:
parent
b359070a0e
commit
681d2b5fe8
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
"git.ignoreLimitWarning": true,
|
||||
"kiro.chat.autoSummarize.threshold": 30
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
|
|
@ -141,6 +142,18 @@ async function fetchList() {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteChat(row) {
|
||||
try {
|
||||
const groupId = row.imGroupId || `order_${row.id}`
|
||||
await request.delete(`/admin/chat-list/${groupId}`)
|
||||
ElMessage.success('已删除')
|
||||
// 从列表中移除
|
||||
list.value = list.value.filter(item => item.id !== row.id)
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function viewChat(row) {
|
||||
chatDialogTitle.value = `聊天记录 - ${row.orderNo} (${row.ownerNickname} ↔ ${row.runnerNickname})`
|
||||
chatDialogVisible.value = true
|
||||
|
|
@ -148,14 +161,9 @@ async function viewChat(row) {
|
|||
chatMessages.value = []
|
||||
|
||||
try {
|
||||
const params = {}
|
||||
// 优先用群ID(群聊模式)
|
||||
if (row.imGroupId) {
|
||||
params.groupId = row.imGroupId
|
||||
} else {
|
||||
// 兼容旧数据
|
||||
params.groupId = `order_${row.id}`
|
||||
}
|
||||
// 统一用群ID获取聊天记录
|
||||
const groupId = row.imGroupId || `order_${row.id}`
|
||||
const params = { groupId }
|
||||
const res = await request.get('/admin/chat-messages', { params })
|
||||
chatMessages.value = parseIMMessages(res, row)
|
||||
} catch (e) {
|
||||
|
|
@ -190,7 +198,7 @@ function parseIMMessages(imResponse, orderInfo) {
|
|||
const showTime = timestamp - lastTime > 300000
|
||||
let timeLabel = ''
|
||||
if (showTime && timestamp) {
|
||||
timeLabel = formatTime(timestamp / 1000 > 1e12 ? timestamp : timestamp * 1000)
|
||||
timeLabel = formatTime(new Date(timestamp))
|
||||
lastTime = timestamp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@
|
|||
<el-radio-group v-model="statusFilter" @change="loadData">
|
||||
<el-radio-button label="">全部</el-radio-button>
|
||||
<el-radio-button label="Pending">待处理</el-radio-button>
|
||||
<el-radio-button label="Processing">处理中</el-radio-button>
|
||||
<el-radio-button label="Completed">已完成</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" stripe v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="userNickname" label="用户" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="userNickname" label="用户" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.userNickname }} <span style="color:#999;font-size:11px;">{{ row.userUid }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #e64340; font-weight: bold">¥{{ row.amount }}</span>
|
||||
|
|
@ -25,13 +28,6 @@
|
|||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款码" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.qrCodeImage" :src="row.qrCodeImage" :preview-src-list="[row.qrCodeImage]"
|
||||
style="width: 40px; height: 40px" fit="cover" preview-teleported />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" round size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
|
|
@ -42,12 +38,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 'Pending'">
|
||||
<el-button type="warning" size="small" plain @click="handleAction(row, 'processing')">处理中</el-button>
|
||||
<el-button type="success" size="small" plain @click="handleAction(row, 'approve')">通过</el-button>
|
||||
<el-button type="danger" size="small" plain @click="handleAction(row, 'reject')">拒绝</el-button>
|
||||
</template>
|
||||
<template v-else-if="row.status === 'Processing'">
|
||||
<template v-if="row.status === 'Pending' || row.status === 'Processing'">
|
||||
<el-button type="success" size="small" plain @click="handleAction(row, 'approve')">通过</el-button>
|
||||
<el-button type="danger" size="small" plain @click="handleAction(row, 'reject')">拒绝</el-button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,53 @@
|
|||
<script>
|
||||
import { initIM } from './utils/im'
|
||||
import { initIM, getConversationList } from './utils/im'
|
||||
|
||||
export default {
|
||||
globalData: {
|
||||
badgeTimer: null
|
||||
},
|
||||
onLaunch: function() {
|
||||
// 检查本地登录凭证,过期则静默清除,不跳转登录页
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length === 3) {
|
||||
const payload = JSON.parse(atob(parts[1]))
|
||||
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
||||
// token 已过期,静默清除
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,清除无效 token
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
return
|
||||
}
|
||||
|
||||
// token 有效,后台初始化 IM
|
||||
initIM().catch(e => {
|
||||
console.log('[App] IM 初始化失败(非阻塞):', e.message)
|
||||
})
|
||||
},
|
||||
onShow: function() {},
|
||||
onHide: function() {}
|
||||
onShow: function() {
|
||||
this.updateTabBarBadge()
|
||||
// 全局定时刷新 tabBar 未读数(每10秒)
|
||||
if (!this.globalData.badgeTimer) {
|
||||
this.globalData.badgeTimer = setInterval(() => {
|
||||
this.updateTabBarBadge()
|
||||
}, 10000)
|
||||
}
|
||||
},
|
||||
onHide: function() {
|
||||
if (this.globalData.badgeTimer) {
|
||||
clearInterval(this.globalData.badgeTimer)
|
||||
this.globalData.badgeTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateTabBarBadge() {
|
||||
try {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) return
|
||||
// 仅在 TabBar 页面才操作角标,避免非 TabBar 页面报错
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const tabBarPaths = ['pages/index/index', 'pages/order-hall/order-hall', 'pages/message/message', 'pages/mine/mine']
|
||||
if (!currentPage || !tabBarPaths.includes(currentPage.route)) return
|
||||
const convList = await getConversationList()
|
||||
const total = convList.reduce((sum, c) => sum + (c.unreadCount || 0), 0)
|
||||
if (total > 0) {
|
||||
uni.setTabBarBadge({ index: 2, text: total > 99 ? '99+' : String(total) })
|
||||
} else {
|
||||
uni.removeTabBarBadge({ index: 2 })
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,17 @@
|
|||
const userStore = useUserStore()
|
||||
return this.orderInfo.ownerId === userStore.userId
|
||||
},
|
||||
/** 构建 IM userId -> 头像 的映射 */
|
||||
avatarMap() {
|
||||
const map = {}
|
||||
if (this.orderInfo.ownerId) {
|
||||
map[`user_${this.orderInfo.ownerId}`] = this.orderInfo.ownerAvatar || ''
|
||||
}
|
||||
if (this.orderInfo.runnerId) {
|
||||
map[`user_${this.orderInfo.runnerId}`] = this.orderInfo.runnerAvatar || ''
|
||||
}
|
||||
return map
|
||||
},
|
||||
showGoodsPriceBtn() {
|
||||
const type = this.orderInfo.orderType
|
||||
return type === 'Purchase' || type === 'Food'
|
||||
|
|
@ -329,7 +340,7 @@
|
|||
for (const msg of msgList) {
|
||||
// 只处理当前群的消息
|
||||
if (msg.conversationID === `GROUP${this.imGroupId}`) {
|
||||
const formatted = formatIMMessage(msg, this.imUserId)
|
||||
const formatted = formatIMMessage(msg, this.imUserId, this.avatarMap)
|
||||
// 收到改价响应消息,更新对应改价卡片状态
|
||||
if (formatted.type === 'price-change-response') {
|
||||
const card = this.chatMessages.find(m => m.type === 'price-change' && m.priceChangeId === formatted.priceChangeId)
|
||||
|
|
@ -363,7 +374,7 @@
|
|||
this.loadingHistory = true
|
||||
try {
|
||||
const res = await getMessageList(this.imGroupId, this.nextReqMessageID)
|
||||
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId))
|
||||
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId, this.avatarMap))
|
||||
// 处理历史消息中的改价响应
|
||||
const formatted = []
|
||||
for (const m of allFormatted) {
|
||||
|
|
@ -402,7 +413,7 @@
|
|||
this.inputText = ''
|
||||
try {
|
||||
const msg = await sendTextMessage(this.imGroupId, text)
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId, this.avatarMap))
|
||||
this.scrollToBottom()
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
|
|
@ -435,7 +446,7 @@
|
|||
success: async (res) => {
|
||||
try {
|
||||
const msg = await sendImageMessage(this.imGroupId, res)
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId, this.avatarMap))
|
||||
this.scrollToBottom()
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
|
|
@ -567,7 +578,7 @@
|
|||
// 发一条文本消息记录操作(群内可见,持久化)
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.imGroupId, `[系统提示] 单主已${actionLabel}${changeTypeLabel}改价`)
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||||
} catch (ex) {}
|
||||
} else {
|
||||
this.chatMessages.push({
|
||||
|
|
@ -586,7 +597,7 @@
|
|||
if (this.imReady && this.imGroupId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||||
} catch (ex) {}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -733,7 +744,7 @@
|
|||
if (this.imReady && this.imGroupId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||||
this.scrollToBottom()
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,16 @@ export default {
|
|||
this.loadUnreadCount()
|
||||
this.loadChatList()
|
||||
this.updateTabBarBadge()
|
||||
// 定时刷新未读数(每5秒)
|
||||
this._refreshTimer = setInterval(() => {
|
||||
this.loadChatList()
|
||||
}, 5000)
|
||||
},
|
||||
onHide() {
|
||||
if (this._refreshTimer) {
|
||||
clearInterval(this._refreshTimer)
|
||||
this._refreshTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 加载未读消息数 */
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export default {
|
|||
/** 格式化时间(精确到年月日时分,) */
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default {
|
|||
/** 格式化时间 */
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export default {
|
|||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export default {
|
|||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
},
|
||||
|
|
@ -244,8 +244,7 @@ export default {
|
|||
try {
|
||||
await applyWithdraw({
|
||||
amount,
|
||||
paymentMethod: 'WeChat',
|
||||
qrCodeImage: ''
|
||||
paymentMethod: 'WeChat'
|
||||
})
|
||||
|
||||
uni.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@
|
|||
<view class="profile-section">
|
||||
<view class="profile-item">
|
||||
<text class="item-label">头像</text>
|
||||
<view class="item-right">
|
||||
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image class="avatar-preview" :src="form.avatarUrl || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<image class="arrow-icon" src="/static/ic_arrow.png" mode="aspectFit"></image>
|
||||
</button>
|
||||
<view class="item-right" @click="onPickAvatar">
|
||||
<image class="avatar-preview" :src="form.avatarUrl || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<image class="arrow-icon" src="/static/ic_arrow.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-item">
|
||||
|
|
@ -64,20 +62,28 @@ export default {
|
|||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
|
||||
/** 微信选择头像回调 */
|
||||
async onChooseAvatar(e) {
|
||||
const tempUrl = e.detail.avatarUrl
|
||||
if (!tempUrl) return
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
const uploadRes = await uploadFile(tempUrl)
|
||||
this.form.avatarUrl = uploadRes.url
|
||||
uni.hideLoading()
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
// 上传失败,先用临时路径显示
|
||||
this.form.avatarUrl = tempUrl
|
||||
}
|
||||
/** 选择头像 */
|
||||
async onPickAvatar() {
|
||||
const self = this
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempUrl = res.tempFilePaths[0]
|
||||
if (!tempUrl) return
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
const uploadRes = await uploadFile(tempUrl)
|
||||
self.form.avatarUrl = uploadRes.url
|
||||
uni.hideLoading()
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
// 上传失败,先用临时路径显示
|
||||
self.form.avatarUrl = tempUrl
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 昵称输入框失焦(微信昵称选择后触发) */
|
||||
|
|
|
|||
|
|
@ -199,6 +199,18 @@ export default {
|
|||
if (!res.confirm) return
|
||||
try {
|
||||
await confirmOrder(this.orderId)
|
||||
|
||||
// 通过 IM 通知跑腿
|
||||
try {
|
||||
await initIM()
|
||||
const groupId = this.orderInfo.imGroupId || `order_${this.orderId}`
|
||||
await sendCustomMessage(groupId, {
|
||||
bizType: 'order-status',
|
||||
action: 'WaitConfirm→Completed',
|
||||
description: '单主已确认完成,订单已结束'
|
||||
})
|
||||
} catch (ex) {}
|
||||
|
||||
uni.showToast({ title: '订单已完成', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
|
|
@ -219,6 +231,18 @@ export default {
|
|||
if (!res.confirm) return
|
||||
try {
|
||||
await rejectOrder(this.orderId)
|
||||
|
||||
// 通过 IM 通知跑腿
|
||||
try {
|
||||
await initIM()
|
||||
const groupId = this.orderInfo.imGroupId || `order_${this.orderId}`
|
||||
await sendCustomMessage(groupId, {
|
||||
bizType: 'order-status',
|
||||
action: 'WaitConfirm→InProgress',
|
||||
description: '单主拒绝确认完成,订单继续进行'
|
||||
})
|
||||
} catch (ex) {}
|
||||
|
||||
uni.showToast({ title: '已拒绝,订单继续进行', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
|
|
@ -233,7 +257,7 @@ export default {
|
|||
/** 格式化时间(精确至年月日时分) */
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export default {
|
|||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
},
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export default {
|
|||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
},
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ export default {
|
|||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,19 @@ export async function initIM() {
|
|||
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
||||
console.log('[IM] SDK 就绪')
|
||||
isReady = true
|
||||
// SDK就绪后同步用户头像和昵称到腾讯 IM
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
const parsed = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
|
||||
chat.updateMyProfile({
|
||||
nick: parsed.nickname || '',
|
||||
avatar: parsed.avatarUrl || ''
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[IM] 同步资料失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, (event) => {
|
||||
|
|
@ -169,9 +182,9 @@ export async function sendCustomMessage(groupId, customData) {
|
|||
}
|
||||
|
||||
/** 将 IM 消息转换为聊天页展示格式 */
|
||||
export function formatIMMessage(msg, currentImUserId) {
|
||||
export function formatIMMessage(msg, currentImUserId, avatarMap = {}) {
|
||||
const isSelf = msg.from === currentImUserId
|
||||
const avatar = msg.avatar || '/static/logo.png'
|
||||
const avatar = avatarMap[msg.from] || msg.avatar || '/static/logo.png'
|
||||
|
||||
// 文本消息
|
||||
if (msg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
||||
|
|
|
|||
|
|
@ -601,6 +601,7 @@ public static class AdminEndpoints
|
|||
o.ItemName,
|
||||
Status = o.Status.ToString(),
|
||||
o.Commission,
|
||||
o.ImGroupId,
|
||||
OwnerId = o.OwnerId,
|
||||
OwnerUid = o.Owner!.Uid,
|
||||
OwnerNickname = o.Owner!.Nickname,
|
||||
|
|
@ -621,6 +622,7 @@ public static class AdminEndpoints
|
|||
o.ItemName,
|
||||
o.Status,
|
||||
o.Commission,
|
||||
o.ImGroupId,
|
||||
o.OwnerId,
|
||||
OwnerUid = string.IsNullOrWhiteSpace(o.OwnerUid) ? o.OwnerId.ToString() : o.OwnerUid,
|
||||
OwnerNickname = string.IsNullOrWhiteSpace(o.OwnerNickname) ? $"用户{o.OwnerId}" : o.OwnerNickname,
|
||||
|
|
@ -661,5 +663,29 @@ public static class AdminEndpoints
|
|||
return Results.BadRequest(new { code = 400, message = $"拉取聊天记录失败: {ex.Message}" });
|
||||
}
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端删除聊天记录(解散IM群并清除订单群ID)
|
||||
app.MapDelete("/api/admin/chat-list/{groupId}", async (string groupId, AppDbContext db, TencentIMService imService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 解散 IM 群
|
||||
await imService.DestroyGroupAsync(groupId);
|
||||
|
||||
// 清除订单的群ID
|
||||
var order = await db.Orders.FirstOrDefaultAsync(o => o.ImGroupId == groupId);
|
||||
if (order != null)
|
||||
{
|
||||
order.ImGroupId = null;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Results.Ok(new { message = "已删除" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.Message}" });
|
||||
}
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ public static class EarningEndpoints
|
|||
UserId = userId,
|
||||
Amount = request.Amount,
|
||||
PaymentMethod = paymentMethod,
|
||||
QrCodeImage = request.QrCodeImage,
|
||||
Status = WithdrawalStatus.Pending,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
|
@ -205,9 +204,9 @@ public static class EarningEndpoints
|
|||
w.Id,
|
||||
w.UserId,
|
||||
UserNickname = w.User!.Nickname ?? ("用户" + w.UserId),
|
||||
UserUid = w.User!.Uid ?? w.UserId.ToString(),
|
||||
w.Amount,
|
||||
PaymentMethod = w.PaymentMethod.ToString(),
|
||||
w.QrCodeImage,
|
||||
Status = w.Status.ToString(),
|
||||
w.CreatedAt,
|
||||
w.ProcessedAt
|
||||
|
|
@ -229,20 +228,20 @@ public static class EarningEndpoints
|
|||
|
||||
if (request.Action == "approve")
|
||||
{
|
||||
// 先调用微信商家转账到零钱
|
||||
// 调用微信商家转账到零钱
|
||||
var user = await db.Users.FindAsync(withdrawal.UserId);
|
||||
if (user == null || string.IsNullOrEmpty(user.OpenId))
|
||||
return Results.BadRequest(new { code = 400, message = "用户信息异常,无法转账" });
|
||||
|
||||
var amountFen = (int)(withdrawal.Amount * 100);
|
||||
var batchNo = $"W{withdrawal.Id}_{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
var detailNo = $"D{withdrawal.Id}_{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
var batchNo = $"W{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
var detailNo = $"D{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
var (transferSuccess, transferError) = await wxPay.TransferToWallet(batchNo, detailNo, user.OpenId, amountFen, "跑腿提现到账");
|
||||
|
||||
if (!transferSuccess)
|
||||
{
|
||||
Console.WriteLine($"[提现] 转账失败: {transferError}");
|
||||
return Results.BadRequest(new { code = 400, message = $"转账失败,请稍后重试", detail = transferError });
|
||||
return Results.BadRequest(new { code = 400, message = "转账失败,请稍后重试", detail = transferError });
|
||||
}
|
||||
|
||||
withdrawal.Status = WithdrawalStatus.Completed;
|
||||
|
|
|
|||
|
|
@ -145,10 +145,13 @@ public static class MessageEndpoints
|
|||
Id = o.Id,
|
||||
OrderNo = o.OrderNo,
|
||||
OrderType = o.OrderType.ToString(),
|
||||
Title = o.Status == OrderStatus.InProgress ? "订单已被接取"
|
||||
: o.Status == OrderStatus.Completed ? "订单已完成"
|
||||
Title = o.Status == OrderStatus.InProgress
|
||||
? (o.OwnerId == userId ? "订单已被接取" : "您已接取订单")
|
||||
: o.Status == OrderStatus.Completed
|
||||
? (o.RunnerId == userId ? "单主已确认完成" : "订单已完成")
|
||||
: o.Status == OrderStatus.Cancelled ? "订单已取消"
|
||||
: o.Status == OrderStatus.WaitConfirm ? "订单待确认"
|
||||
: o.Status == OrderStatus.WaitConfirm
|
||||
? (o.OwnerId == userId ? "跑腿已提交完成,请确认" : "等待单主确认完成")
|
||||
: o.Status == OrderStatus.Appealing ? "订单申诉中"
|
||||
: "订单状态变更",
|
||||
ItemName = o.ItemName,
|
||||
|
|
|
|||
|
|
@ -332,6 +332,8 @@ public static class OrderEndpoints
|
|||
AcceptedAt = visibleAcceptedAt,
|
||||
CompletedAt = visibleCompletedAt,
|
||||
RunnerNickname = runnerNickname,
|
||||
RunnerAvatar = order.Runner?.AvatarUrl,
|
||||
OwnerAvatar = order.Owner?.AvatarUrl,
|
||||
RunnerUid = runnerUid,
|
||||
RunnerPhone = runnerPhone
|
||||
};
|
||||
|
|
@ -513,6 +515,7 @@ public static class OrderEndpoints
|
|||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
Console.WriteLine($"[接单] 订单 {order.Id} 接单成功,准备创建群组");
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
|
|
@ -525,16 +528,19 @@ public static class OrderEndpoints
|
|||
var groupId = $"order_{order.Id}";
|
||||
var ownerImId = $"user_{order.OwnerId}";
|
||||
var runnerImId = $"user_{userId}";
|
||||
Console.WriteLine($"[IM] 准备创建群组: {groupId}, 单主: {ownerImId}, 跑腿: {runnerImId}");
|
||||
var result = await imService.CreateGroupAsync(groupId, $"订单{order.OrderNo}", ownerImId, runnerImId);
|
||||
Console.WriteLine($"[IM] 创建群组结果: {result}");
|
||||
if (result != null)
|
||||
{
|
||||
order.ImGroupId = result;
|
||||
await db.SaveChangesAsync();
|
||||
Console.WriteLine($"[IM] 群组ID已保存: {result}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[IM] 创建群组失败: {ex.Message}");
|
||||
Console.WriteLine($"[IM] 创建群组失败: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
return Results.Ok(new AcceptOrderResponse
|
||||
|
|
|
|||
|
|
@ -61,10 +61,6 @@ public class WithdrawRequest
|
|||
/// <summary>收款方式:WeChat 或 Alipay</summary>
|
||||
[Required(ErrorMessage = "收款方式不能为空")]
|
||||
public string PaymentMethod { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>收款二维码图片</summary>
|
||||
[Required(ErrorMessage = "收款二维码不能为空")]
|
||||
public string QrCodeImage { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ public class OrderResponse
|
|||
public DateTime? CompletedAt { get; set; }
|
||||
/// <summary>跑腿昵称</summary>
|
||||
public string? RunnerNickname { get; set; }
|
||||
/// <summary>跑腿头像</summary>
|
||||
public string? RunnerAvatar { get; set; }
|
||||
/// <summary>单主头像</summary>
|
||||
public string? OwnerAvatar { get; set; }
|
||||
/// <summary>跑腿 UID</summary>
|
||||
public int? RunnerUid { get; set; }
|
||||
/// <summary>跑腿手机号(认证时填写的手机号,仅单主可见)</summary>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ public class TencentIMService
|
|||
|
||||
var body = new
|
||||
{
|
||||
Type = "Private",
|
||||
Type = "Work",
|
||||
GroupId = groupId,
|
||||
Name = groupName,
|
||||
Owner_Account = ownerImId,
|
||||
|
|
@ -131,16 +131,31 @@ public class TencentIMService
|
|||
var random = Random.Shared.Next(100000, 999999);
|
||||
var url = $"https://console.tim.qq.com/v4/group_open_http_svc/group_msg_get_simple?sdkappid={_sdkAppId}&identifier={AdminAccount}&usersig={adminSig}&random={random}&contenttype=json";
|
||||
|
||||
var body = new
|
||||
{
|
||||
GroupId = groupId,
|
||||
ReqMsgSeq = reqMsgSeq,
|
||||
ReqMsgNumber = reqMsgNumber
|
||||
};
|
||||
// ReqMsgSeq 为 0 时不传该字段,让腾讯 IM 自动返回最新消息
|
||||
object body = reqMsgSeq > 0
|
||||
? new { GroupId = groupId, ReqMsgSeq = reqMsgSeq, ReqMsgNumber = reqMsgNumber }
|
||||
: new { GroupId = groupId, ReqMsgNumber = reqMsgNumber };
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(url, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"[IM] 拉取群消息 {groupId}: {json.Substring(0, Math.Min(json.Length, 500))}");
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解散 IM 群组
|
||||
/// </summary>
|
||||
public async Task DestroyGroupAsync(string groupId)
|
||||
{
|
||||
var adminSig = GenerateUserSig(AdminAccount);
|
||||
var random = Random.Shared.Next(100000, 999999);
|
||||
var url = $"https://console.tim.qq.com/v4/group_open_http_svc/destroy_group?sdkappid={_sdkAppId}&identifier={AdminAccount}&usersig={adminSig}&random={random}&contenttype=json";
|
||||
|
||||
var body = new { GroupId = groupId };
|
||||
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(url, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"[IM] 解散群组 {groupId}: {json}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user