This commit is contained in:
parent
5a04e003ca
commit
b359070a0e
|
|
@ -148,9 +148,15 @@ async function viewChat(row) {
|
|||
chatMessages.value = []
|
||||
|
||||
try {
|
||||
const res = await request.get('/admin/chat-messages', {
|
||||
params: { ownerUserId: row.ownerId, runnerUserId: row.runnerId }
|
||||
})
|
||||
const params = {}
|
||||
// 优先用群ID(群聊模式)
|
||||
if (row.imGroupId) {
|
||||
params.groupId = row.imGroupId
|
||||
} else {
|
||||
// 兼容旧数据
|
||||
params.groupId = `order_${row.id}`
|
||||
}
|
||||
const res = await request.get('/admin/chat-messages', { params })
|
||||
chatMessages.value = parseIMMessages(res, row)
|
||||
} catch (e) {
|
||||
console.error('拉取聊天记录失败:', e)
|
||||
|
|
@ -160,7 +166,8 @@ async function viewChat(row) {
|
|||
}
|
||||
|
||||
function parseIMMessages(imResponse, orderInfo) {
|
||||
const msgList = imResponse.MsgList || imResponse.msgList || []
|
||||
// 群消息格式:RspMsgList
|
||||
const msgList = imResponse.RspMsgList || imResponse.rspMsgList || imResponse.MsgList || imResponse.msgList || []
|
||||
if (!msgList.length) return []
|
||||
|
||||
const ownerImId = `user_${orderInfo.ownerId}`
|
||||
|
|
|
|||
|
|
@ -153,6 +153,13 @@
|
|||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑资料",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/runner/certification",
|
||||
"style": {
|
||||
|
|
|
|||
|
|
@ -379,6 +379,10 @@ export default {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.accept-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 弹窗样式(复用) */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@
|
|||
<script>
|
||||
import {
|
||||
getOrderDetail,
|
||||
getOrderByChatUser,
|
||||
createPriceChange,
|
||||
respondPriceChange as respondPriceChangeApi
|
||||
} from '../../utils/api'
|
||||
|
|
@ -212,7 +211,6 @@
|
|||
} from '../../stores/user'
|
||||
import {
|
||||
initIM,
|
||||
logoutIM,
|
||||
onNewMessage,
|
||||
offNewMessage,
|
||||
getMessageList,
|
||||
|
|
@ -234,7 +232,7 @@
|
|||
scrollTop: 0,
|
||||
showMorePanel: false,
|
||||
imUserId: '',
|
||||
targetImUserId: '',
|
||||
imGroupId: null,
|
||||
imReady: false,
|
||||
nextReqMessageID: '',
|
||||
historyCompleted: false,
|
||||
|
|
@ -316,38 +314,26 @@
|
|||
userId
|
||||
} = await initIM()
|
||||
this.imUserId = userId
|
||||
if (this.targetImUserIdFromParam) {
|
||||
this.targetImUserId = this.targetImUserIdFromParam
|
||||
} else if (this.orderInfo.id) {
|
||||
const userStore = useUserStore()
|
||||
this.targetImUserId = this.orderInfo.ownerId === userStore.userId ?
|
||||
`user_${this.orderInfo.runnerId}` : `user_${this.orderInfo.ownerId}`
|
||||
|
||||
// 从订单信息获取群组ID
|
||||
if (this.orderInfo.imGroupId) {
|
||||
this.imGroupId = this.orderInfo.imGroupId
|
||||
} else if (this.orderId) {
|
||||
// 兼容旧订单:群ID格式为 order_{orderId}
|
||||
this.imGroupId = `order_${this.orderId}`
|
||||
}
|
||||
|
||||
this.imReady = true
|
||||
// 如果没有 orderId 但有对方用户ID,查找关联订单
|
||||
if (!this.orderId && this.targetImUserId) {
|
||||
try {
|
||||
const targetUid = this.targetImUserId.replace('user_', '')
|
||||
const res = await getOrderByChatUser(targetUid)
|
||||
if (res && res.found && res.orderId) {
|
||||
this.orderId = res.orderId
|
||||
await this.loadOrderInfo()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[聊天] 查找关联订单失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onNewMessage((msgList) => {
|
||||
for (const msg of msgList) {
|
||||
if (msg.from === this.targetImUserId || msg.from === this.imUserId) {
|
||||
// 只处理当前群的消息
|
||||
if (msg.conversationID === `GROUP${this.imGroupId}`) {
|
||||
const formatted = formatIMMessage(msg, this.imUserId)
|
||||
// 按订单ID过滤
|
||||
if (this.orderId && formatted.orderId && String(formatted.orderId) !== String(this.orderId)) continue
|
||||
// 收到改价响应消息,更新对应改价卡片状态
|
||||
if (formatted.type === 'price-change-response') {
|
||||
const card = this.chatMessages.find(m => m.type === 'price-change' && m.priceChangeId === formatted.priceChangeId)
|
||||
if (card) card.status = formatted.status
|
||||
// 显示为系统提示
|
||||
this.chatMessages.push({
|
||||
id: formatted.id,
|
||||
type: 'system',
|
||||
|
|
@ -359,13 +345,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
this.scrollToBottom()
|
||||
// 收到新消息时标记已读
|
||||
if (this.targetImUserId) setMessageRead(this.targetImUserId)
|
||||
if (this.imGroupId) setMessageRead(this.imGroupId)
|
||||
})
|
||||
|
||||
await this.loadHistory()
|
||||
// 标记会话已读
|
||||
if (this.targetImUserId) setMessageRead(this.targetImUserId)
|
||||
if (this.imGroupId) setMessageRead(this.imGroupId)
|
||||
} catch (e) {
|
||||
console.error('[IM] 初始化失败:', e)
|
||||
uni.showToast({
|
||||
|
|
@ -374,21 +358,17 @@
|
|||
})
|
||||
}
|
||||
},
|
||||
async loadHistory() {
|
||||
if (!this.imReady || !this.targetImUserId) return
|
||||
async loadHistory(scrollToEnd = true) {
|
||||
if (!this.imReady || !this.imGroupId) return
|
||||
this.loadingHistory = true
|
||||
try {
|
||||
const res = await getMessageList(this.targetImUserId, this.nextReqMessageID)
|
||||
const res = await getMessageList(this.imGroupId, this.nextReqMessageID)
|
||||
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId))
|
||||
// 按订单ID过滤:只显示当前订单的消息,或没有orderId的旧消息
|
||||
const filtered = this.orderId
|
||||
? allFormatted.filter(m => !m.orderId || String(m.orderId) === String(this.orderId))
|
||||
: allFormatted
|
||||
// 处理历史消息中的改价响应,更新对应改价卡片状态
|
||||
// 处理历史消息中的改价响应
|
||||
const formatted = []
|
||||
for (const m of filtered) {
|
||||
for (const m of allFormatted) {
|
||||
if (m.type === 'price-change-response') {
|
||||
const card = filtered.find(c => c.type === 'price-change' && c.priceChangeId === m.priceChangeId)
|
||||
const card = allFormatted.find(c => c.type === 'price-change' && c.priceChangeId === m.priceChangeId)
|
||||
if (card) card.status = m.status
|
||||
formatted.push({ id: m.id, type: 'system', content: `${m.isSelf ? '您' : '对方'}${m.action === 'Accepted' ? '同意' : '拒绝'}了${m.changeTypeLabel}改价` })
|
||||
} else {
|
||||
|
|
@ -398,7 +378,7 @@
|
|||
this.chatMessages = [...formatted, ...this.chatMessages]
|
||||
this.nextReqMessageID = res.nextReqMessageID
|
||||
this.historyCompleted = res.isCompleted
|
||||
this.scrollToBottom()
|
||||
if (scrollToEnd) this.scrollToBottom()
|
||||
} catch (e) {
|
||||
console.error('[IM] 拉取历史消息失败:', e)
|
||||
} finally {
|
||||
|
|
@ -407,7 +387,7 @@
|
|||
},
|
||||
async loadMoreHistory() {
|
||||
if (this.historyCompleted || this.loadingHistory) return
|
||||
await this.loadHistory()
|
||||
await this.loadHistory(false)
|
||||
},
|
||||
async sendMessage() {
|
||||
if (!this.inputText.trim()) return
|
||||
|
|
@ -421,7 +401,7 @@
|
|||
const text = this.inputText
|
||||
this.inputText = ''
|
||||
try {
|
||||
const msg = await sendTextMessage(this.targetImUserId, text, this.orderId)
|
||||
const msg = await sendTextMessage(this.imGroupId, text)
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||||
this.scrollToBottom()
|
||||
} catch (e) {
|
||||
|
|
@ -454,7 +434,7 @@
|
|||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
try {
|
||||
const msg = await sendImageMessage(this.targetImUserId, res, this.orderId)
|
||||
const msg = await sendImageMessage(this.imGroupId, res)
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||||
this.scrollToBottom()
|
||||
} catch (e) {
|
||||
|
|
@ -523,7 +503,7 @@
|
|||
this.showPriceChangePopup = false
|
||||
const changeTypeLabel = this.priceChangeType === 'Commission' ? '跑腿佣金' : '商品总额'
|
||||
if (this.imReady) {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
await sendCustomMessage(this.imGroupId, {
|
||||
bizType: 'price-change',
|
||||
priceChangeId: res.id,
|
||||
changeTypeLabel,
|
||||
|
|
@ -532,7 +512,7 @@
|
|||
difference: res.difference,
|
||||
status: res.status,
|
||||
description: `发起了${changeTypeLabel}改价申请`
|
||||
}, this.orderId)
|
||||
})
|
||||
}
|
||||
this.chatMessages.push({
|
||||
id: `pc_${res.id}`,
|
||||
|
|
@ -559,6 +539,11 @@
|
|||
},
|
||||
async respondPriceChange(priceChangeId, action) {
|
||||
try {
|
||||
console.log('[聊天] 响应改价:', this.orderId, priceChangeId, action)
|
||||
if (!this.orderId) {
|
||||
uni.showToast({ title: '订单信息未加载', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const res = await respondPriceChangeApi(this.orderId, priceChangeId, {
|
||||
action
|
||||
})
|
||||
|
|
@ -570,18 +555,18 @@
|
|||
|
||||
// 通过 IM 通知对方改价结果(对方收到后显示为系统提示)
|
||||
if (this.imReady) {
|
||||
// 发给对方
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
// 发送到群
|
||||
await sendCustomMessage(this.imGroupId, {
|
||||
bizType: 'price-change-response',
|
||||
priceChangeId: priceChangeId,
|
||||
action: action,
|
||||
status: res.status,
|
||||
changeTypeLabel,
|
||||
description: `${actionLabel}了${changeTypeLabel}改价申请`
|
||||
}, this.orderId)
|
||||
// 发一条文本消息记录操作(双方都能看到,持久化)
|
||||
})
|
||||
// 发一条文本消息记录操作(群内可见,持久化)
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, `[系统提示] 单主已${actionLabel}${changeTypeLabel}改价`, this.orderId)
|
||||
const imMsg = await sendTextMessage(this.imGroupId, `[系统提示] 单主已${actionLabel}${changeTypeLabel}改价`)
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
} catch (ex) {}
|
||||
} else {
|
||||
|
|
@ -597,10 +582,10 @@
|
|||
if (res.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(res.paymentParams)
|
||||
// 补缴成功,通过IM通知双方
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
// 补缴成功,通过IM通知群内
|
||||
if (this.imReady && this.imGroupId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, '[系统提示] 单主已完成补缴支付', this.orderId)
|
||||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
|
@ -619,13 +604,13 @@
|
|||
const refundText = res.refundSuccess
|
||||
? `改价退款¥${Math.abs(res.difference).toFixed(2)}已原路返回`
|
||||
: '退款处理中,请稍后查看'
|
||||
// 通过IM发送(持久化,双方可见)
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
// 通过IM发送(持久化,群内可见)
|
||||
if (this.imReady && this.imGroupId) {
|
||||
try {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
await sendCustomMessage(this.imGroupId, {
|
||||
bizType: 'order-status',
|
||||
description: refundText
|
||||
}, this.orderId)
|
||||
})
|
||||
} catch (ex) {}
|
||||
}
|
||||
this.chatMessages.push({ id: `sys_r_${Date.now()}`, type: 'system', content: refundText })
|
||||
|
|
@ -665,26 +650,18 @@
|
|||
const msg = statusMessages[key]
|
||||
if (!msg) return
|
||||
|
||||
// 避免重复:检查最后一条系统消息是否相同
|
||||
const lastMsg = [...this.chatMessages].reverse().find(m => m.type === 'system')
|
||||
if (lastMsg && lastMsg.content === msg) return
|
||||
// 避免重复:检查聊天消息中是否已有相同内容
|
||||
const isDuplicate = this.chatMessages.some(m =>
|
||||
(m.type === 'system' && m.content === msg) ||
|
||||
(m.type === 'text' && m.content && m.content.includes(msg))
|
||||
)
|
||||
if (isDuplicate) return
|
||||
|
||||
// 通过 IM 发送状态变化消息(持久化,双方可见)
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'order-status',
|
||||
action: key,
|
||||
description: msg
|
||||
}, this.orderId)
|
||||
} catch (e) {}
|
||||
}
|
||||
this.chatMessages.push({
|
||||
id: `sys_status_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: msg
|
||||
})
|
||||
this.scrollToBottom()
|
||||
},
|
||||
onCompleteOrder() {
|
||||
this.showMorePanel = false
|
||||
|
|
@ -753,9 +730,9 @@
|
|||
await this.wxPay(this.pendingPayment)
|
||||
uni.showToast({ title: '补缴成功', icon: 'success' })
|
||||
// 发送补缴成功的提示消息
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
if (this.imReady && this.imGroupId) {
|
||||
try {
|
||||
const imMsg = await sendTextMessage(this.targetImUserId, '[系统提示] 单主已完成补缴支付', this.orderId)
|
||||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId))
|
||||
this.scrollToBottom()
|
||||
} catch (ex) {}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,16 @@
|
|||
<!-- 聊天记录用户列表 -->
|
||||
<view class="section-title" v-if="chatList.length > 0">聊天记录</view>
|
||||
<view
|
||||
class="chat-item"
|
||||
class="chat-item-wrap"
|
||||
v-for="item in chatList"
|
||||
:key="item.orderId"
|
||||
>
|
||||
<view
|
||||
class="chat-item"
|
||||
:style="{ transform: `translateX(${item.slideX || 0}px)` }"
|
||||
@touchstart="onTouchStart($event, item)"
|
||||
@touchmove="onTouchMove($event, item)"
|
||||
@touchend="onTouchEnd(item)"
|
||||
@click="goChat(item)"
|
||||
>
|
||||
<view class="avatar-wrap">
|
||||
|
|
@ -69,6 +76,10 @@
|
|||
<text>{{ getStatusLabel(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="delete-btn" @click="onDeleteChat(item)">
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="chatList.length === 0" class="empty-chat">
|
||||
|
|
@ -124,16 +135,16 @@ export default {
|
|||
try {
|
||||
const list = await getChatOrderList()
|
||||
this.chatList = (list || []).map(item => ({ ...item, imUnread: 0 }))
|
||||
// 从 IM 获取未读数
|
||||
// 从 IM 获取群会话未读数
|
||||
try {
|
||||
await initIM()
|
||||
const convList = await getConversationList()
|
||||
const unreadMap = {}
|
||||
convList.forEach(c => { unreadMap[c.targetUserId] = c.unreadCount || 0 })
|
||||
convList.forEach(c => { unreadMap[c.groupId] = c.unreadCount || 0 })
|
||||
let totalImUnread = 0
|
||||
this.chatList.forEach(item => {
|
||||
const imUserId = `user_${item.targetUserId}`
|
||||
item.imUnread = unreadMap[imUserId] || 0
|
||||
const groupId = item.imGroupId || `order_${item.orderId}`
|
||||
item.imUnread = unreadMap[groupId] || 0
|
||||
totalImUnread += item.imUnread
|
||||
})
|
||||
// 更新 tabBar badge(系统未读 + IM未读)
|
||||
|
|
@ -202,7 +213,7 @@ export default {
|
|||
/** 格式化时间显示 */
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const date = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
|
||||
const now = new Date()
|
||||
const isToday = date.toDateString() === now.toDateString()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
|
|
@ -210,6 +221,39 @@ export default {
|
|||
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
},
|
||||
|
||||
/** 触摸开始 */
|
||||
onTouchStart(e, item) {
|
||||
item._startX = e.touches[0].clientX
|
||||
item._startY = e.touches[0].clientY
|
||||
item._moving = false
|
||||
},
|
||||
/** 触摸移动 */
|
||||
onTouchMove(e, item) {
|
||||
const dx = e.touches[0].clientX - item._startX
|
||||
const dy = e.touches[0].clientY - item._startY
|
||||
// 水平滑动才处理
|
||||
if (Math.abs(dx) < Math.abs(dy)) return
|
||||
item._moving = true
|
||||
// 限制滑动范围
|
||||
const x = Math.max(-70, Math.min(0, dx + (item._prevX || 0)))
|
||||
item.slideX = x
|
||||
},
|
||||
/** 触摸结束 */
|
||||
onTouchEnd(item) {
|
||||
// 超过一半就展开,否则收回
|
||||
if (item.slideX < -35) {
|
||||
item.slideX = -70
|
||||
item._prevX = -70
|
||||
} else {
|
||||
item.slideX = 0
|
||||
item._prevX = 0
|
||||
}
|
||||
},
|
||||
/** 删除聊天记录 */
|
||||
onDeleteChat(item) {
|
||||
this.chatList = this.chatList.filter(c => c.orderId !== item.orderId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,13 +372,40 @@ export default {
|
|||
margin: 30rpx 0 16rpx;
|
||||
}
|
||||
|
||||
.chat-item-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 30rpx;
|
||||
margin-bottom: 16rpx;
|
||||
transition: transform 0.2s;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 70px;
|
||||
background: #ff4d4f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0 16rpx 16rpx 0;
|
||||
}
|
||||
|
||||
.delete-btn text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chat-avatar {
|
||||
|
|
|
|||
|
|
@ -86,28 +86,8 @@
|
|||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">收款方式</text>
|
||||
<view class="payment-options">
|
||||
<view
|
||||
class="payment-option"
|
||||
:class="{ active: withdrawForm.paymentMethod === 'WeChat' }"
|
||||
@click="withdrawForm.paymentMethod = 'WeChat'"
|
||||
><text>微信</text></view>
|
||||
<view
|
||||
class="payment-option"
|
||||
:class="{ active: withdrawForm.paymentMethod === 'Alipay' }"
|
||||
@click="withdrawForm.paymentMethod = 'Alipay'"
|
||||
><text>支付宝</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">收款二维码</text>
|
||||
<view class="upload-area" @click="chooseQrImage">
|
||||
<image v-if="withdrawForm.qrImage" class="qr-preview" :src="withdrawForm.qrImage" mode="aspectFit"></image>
|
||||
<text v-else class="upload-placeholder">点击上传</text>
|
||||
</view>
|
||||
<view class="withdraw-tip">
|
||||
<text>提现将自动转入您的微信零钱</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
|
|
@ -260,24 +240,12 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
if (!this.withdrawForm.qrImage) {
|
||||
uni.showToast({ title: '请上传收款二维码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.withdrawSubmitting = true
|
||||
try {
|
||||
// 上传二维码图片
|
||||
let qrCodeImage = this.withdrawForm.qrImageUrl
|
||||
if (this.withdrawForm.qrImage && !qrCodeImage) {
|
||||
const uploadRes = await uploadFile(this.withdrawForm.qrImage)
|
||||
qrCodeImage = uploadRes.url
|
||||
}
|
||||
|
||||
await applyWithdraw({
|
||||
amount,
|
||||
paymentMethod: this.withdrawForm.paymentMethod,
|
||||
qrCodeImage
|
||||
paymentMethod: 'WeChat',
|
||||
qrCodeImage: ''
|
||||
})
|
||||
|
||||
uni.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
|
|
@ -570,6 +538,16 @@ export default {
|
|||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.withdraw-tip {
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.withdraw-tip text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ export default {
|
|||
onUserClick() {
|
||||
if (!this.isLoggedIn) {
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/mine/profile' })
|
||||
}
|
||||
},
|
||||
goMyOrders(status) {
|
||||
|
|
|
|||
164
miniapp/pages/mine/profile.vue
Normal file
164
miniapp/pages/mine/profile.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<view class="profile-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="navbar-title">编辑资料</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<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>
|
||||
</view>
|
||||
<view class="profile-item">
|
||||
<text class="item-label">昵称</text>
|
||||
<view class="item-right">
|
||||
<input class="nickname-input" type="nickname" v-model="form.nickname" placeholder="请输入昵称" maxlength="20" @blur="onNicknameBlur" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-section">
|
||||
<button class="save-btn" @click="onSave" :loading="saving" :disabled="saving">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { updateProfile } from '../../utils/api'
|
||||
import { uploadFile } from '../../utils/request'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
form: {
|
||||
nickname: '',
|
||||
avatarUrl: ''
|
||||
},
|
||||
saving: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
const userStore = useUserStore()
|
||||
this.form.nickname = userStore.userInfo.nickname || ''
|
||||
this.form.avatarUrl = userStore.userInfo.avatarUrl || ''
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
/** 昵称输入框失焦(微信昵称选择后触发) */
|
||||
onNicknameBlur(e) {
|
||||
if (e.detail.value) {
|
||||
this.form.nickname = e.detail.value
|
||||
}
|
||||
},
|
||||
|
||||
/** 保存 */
|
||||
async onSave() {
|
||||
if (!this.form.nickname.trim()) {
|
||||
uni.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await updateProfile({
|
||||
nickname: this.form.nickname.trim(),
|
||||
avatarUrl: this.form.avatarUrl
|
||||
})
|
||||
// 更新本地用户信息
|
||||
const userStore = useUserStore()
|
||||
const info = { ...userStore.userInfo, nickname: res.nickname, avatarUrl: res.avatarUrl }
|
||||
userStore.setLoginInfo(userStore.token, info)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => { uni.navigateBack() }, 1000)
|
||||
} catch (e) {
|
||||
// 错误已在 request 中处理
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.custom-navbar { position: fixed; top: 0; left: 0; width: 100%; z-index: 999; background: #FFB700; }
|
||||
.navbar-content { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 20rpx; }
|
||||
.nav-back { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.back-icon { width: 40rpx; height: 40rpx; }
|
||||
.navbar-title { font-size: 34rpx; font-weight: bold; color: #363636; }
|
||||
.nav-placeholder { width: 60rpx; }
|
||||
|
||||
.profile-section {
|
||||
margin: 20rpx 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
.profile-item:last-child { border-bottom: none; }
|
||||
.item-label { font-size: 30rpx; color: #333; flex-shrink: 0; }
|
||||
.item-right { display: flex; align-items: center; flex: 1; justify-content: flex-end; }
|
||||
.avatar-preview { width: 100rpx; height: 100rpx; border-radius: 50%; margin-right: 16rpx; }
|
||||
.avatar-btn {
|
||||
display: flex; align-items: center; background: none; border: none;
|
||||
padding: 0; margin: 0; line-height: normal;
|
||||
}
|
||||
.avatar-btn::after { border: none; }
|
||||
.arrow-icon { width: 28rpx; height: 28rpx; }
|
||||
.nickname-input { text-align: right; font-size: 30rpx; color: #333; flex: 1; }
|
||||
.wx-tip { font-size: 24rpx; color: #999; margin-right: 12rpx; }
|
||||
|
||||
.save-section { padding: 40rpx 24rpx; }
|
||||
.save-btn {
|
||||
width: 100%; height: 88rpx; line-height: 88rpx;
|
||||
background: #FAD146; color: #fff; font-size: 32rpx;
|
||||
font-weight: bold; border-radius: 44rpx; border: none;
|
||||
}
|
||||
.save-btn::after { border: none; }
|
||||
.save-btn[disabled] { opacity: 0.6; }
|
||||
</style>
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<text class="card-item-name">门店数量:{{ order.shopCount || 0 }}个</text>
|
||||
<view class="card-commission">
|
||||
<text class="commission-label">跑腿费:</text>
|
||||
<text class="commission-value">{{ order.commission }}元</text>
|
||||
<text class="commission-value">{{ formatMoney(order.commission) }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 美食数量 -->
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
<text class="card-item-name">{{ getItemTitle(order) }}</text>
|
||||
<view class="card-commission">
|
||||
<text class="commission-label">跑腿费:</text>
|
||||
<text class="commission-value">{{ order.commission }}元</text>
|
||||
<text class="commission-value">{{ formatMoney(order.commission) }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -293,7 +293,11 @@
|
|||
getHelpTotalAmount(order) {
|
||||
const commission = parseFloat(order.commission) || 0
|
||||
const goodsAmount = parseFloat(order.goodsAmount) || 0
|
||||
return (commission + goodsAmount).toFixed(1)
|
||||
return (commission + goodsAmount).toFixed(2)
|
||||
},
|
||||
/** 格式化金额为2位小数 */
|
||||
formatMoney(val) {
|
||||
return (parseFloat(val) || 0).toFixed(2)
|
||||
},
|
||||
/** 获取卡片标题 */
|
||||
getItemTitle(order) {
|
||||
|
|
|
|||
|
|
@ -169,12 +169,12 @@ export default {
|
|||
// 通过 IM 通知单主
|
||||
try {
|
||||
await initIM()
|
||||
const targetImUserId = `user_${this.orderInfo.ownerId}`
|
||||
await sendCustomMessage(targetImUserId, {
|
||||
const groupId = this.orderInfo.imGroupId || `order_${this.orderId}`
|
||||
await sendCustomMessage(groupId, {
|
||||
bizType: 'order-status',
|
||||
action: 'InProgress→WaitConfirm',
|
||||
description: '跑腿已提交完成,请在订单详情中确认'
|
||||
}, this.orderId)
|
||||
})
|
||||
} catch (ex) {}
|
||||
|
||||
uni.showToast({ title: '已提交完成', icon: 'success' })
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@
|
|||
<script>
|
||||
import { getOrderDetail, cancelOrder, submitReview, confirmOrder, rejectOrder } from '../../utils/api'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { initIM, sendCustomMessage } from '../../utils/im'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -378,10 +379,17 @@ export default {
|
|||
try {
|
||||
await confirmOrder(this.orderId)
|
||||
uni.showToast({ title: '订单已完成', icon: 'success' })
|
||||
// 通知跑腿
|
||||
try {
|
||||
await initIM()
|
||||
const groupId = this.order.imGroupId || `order_${this.orderId}`
|
||||
await sendCustomMessage(groupId, {
|
||||
bizType: 'order-status',
|
||||
description: '订单已完成'
|
||||
})
|
||||
} catch (ex) {}
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
// 错误已在 request 中处理
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
@ -396,10 +404,17 @@ export default {
|
|||
try {
|
||||
await rejectOrder(this.orderId)
|
||||
uni.showToast({ title: '已拒绝,订单继续进行', icon: 'none' })
|
||||
// 通知跑腿
|
||||
try {
|
||||
await initIM()
|
||||
const groupId2 = this.order.imGroupId || `order_${this.orderId}`
|
||||
await sendCustomMessage(groupId2, {
|
||||
bizType: 'order-status',
|
||||
description: '单主已拒绝完成,订单继续进行'
|
||||
})
|
||||
} catch (ex) {}
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
// 错误已在 request 中处理
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export function wxLogin(data) {
|
|||
return request({ url: '/api/auth/wx-login', method: 'POST', data })
|
||||
}
|
||||
|
||||
/** 更新用户信息 */
|
||||
export function updateProfile(data) {
|
||||
return request({ url: '/api/user/profile', method: 'PUT', data })
|
||||
}
|
||||
|
||||
// ==================== Banner ====================
|
||||
|
||||
/** 获取已启用的 Banner 列表 */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 腾讯 IM SDK 封装
|
||||
* 负责初始化、登录、收发消息
|
||||
* 腾讯 IM SDK 封装(群聊模式)
|
||||
* 每个订单一个群,消息按订单隔离
|
||||
*/
|
||||
import TencentCloudChat from '@tencentcloud/chat'
|
||||
import TIMUploadPlugin from 'tim-upload-plugin'
|
||||
|
|
@ -10,131 +10,72 @@ let chat = null
|
|||
let isReady = false
|
||||
let onMessageCallback = null
|
||||
|
||||
/**
|
||||
* 获取 IM 实例(单例)
|
||||
*/
|
||||
/** 获取 IM 实例 */
|
||||
export function getChatInstance() {
|
||||
return chat
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化并登录 IM
|
||||
* @returns {Promise<{sdkAppId, userId}>}
|
||||
*/
|
||||
/** 初始化并登录 IM */
|
||||
export async function initIM() {
|
||||
// 从后端获取 UserSig
|
||||
const res = await request({ url: '/api/im/usersig' })
|
||||
const { sdkAppId, userId, userSig } = res
|
||||
|
||||
// 创建 SDK 实例
|
||||
if (!chat) {
|
||||
chat = TencentCloudChat.create({ SDKAppID: sdkAppId })
|
||||
// 注册上传插件(发送图片/文件必需)
|
||||
chat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin })
|
||||
chat.setLogLevel(1) // Release 级别日志
|
||||
chat.setLogLevel(1)
|
||||
|
||||
// 监听 SDK 就绪
|
||||
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
||||
console.log('[IM] SDK 就绪')
|
||||
isReady = true
|
||||
})
|
||||
|
||||
// 监听新消息
|
||||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, (event) => {
|
||||
const msgList = event.data
|
||||
if (onMessageCallback) {
|
||||
onMessageCallback(msgList)
|
||||
}
|
||||
if (onMessageCallback) onMessageCallback(event.data)
|
||||
})
|
||||
|
||||
// 监听被踢下线
|
||||
chat.on(TencentCloudChat.EVENT.KICKED_OUT, () => {
|
||||
console.log('[IM] 被踢下线')
|
||||
isReady = false
|
||||
})
|
||||
}
|
||||
|
||||
// 登录
|
||||
await chat.login({ userID: userId, userSig })
|
||||
console.log('[IM] 登录成功:', userId)
|
||||
|
||||
// 同步用户资料(昵称+头像)到 IM
|
||||
try {
|
||||
const userInfo = JSON.parse(uni.getStorageSync('userInfo') || '{}')
|
||||
if (userInfo.nickname || userInfo.avatarUrl) {
|
||||
const profileData = {}
|
||||
if (userInfo.nickname) profileData.nick = userInfo.nickname
|
||||
if (userInfo.avatarUrl) profileData.avatar = userInfo.avatarUrl
|
||||
await chat.updateMyProfile(profileData)
|
||||
console.log('[IM] 用户资料已同步')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[IM] 同步用户资料失败:', e)
|
||||
}
|
||||
|
||||
return { sdkAppId, userId }
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出 IM
|
||||
*/
|
||||
/** 登出 IM */
|
||||
export async function logoutIM() {
|
||||
if (chat) {
|
||||
await chat.logout()
|
||||
isReady = false
|
||||
console.log('[IM] 已登出')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置新消息回调
|
||||
* @param {Function} callback - 接收消息列表的回调
|
||||
*/
|
||||
/** 设置新消息回调 */
|
||||
export function onNewMessage(callback) {
|
||||
onMessageCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除新消息回调
|
||||
*/
|
||||
/** 移除新消息回调 */
|
||||
export function offNewMessage() {
|
||||
onMessageCallback = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话 ID(C2C 单聊)
|
||||
* @param {string} targetUserId - 对方用户 ID(如 user_123)
|
||||
*/
|
||||
export function getConversationId(targetUserId) {
|
||||
return `C2C${targetUserId}`
|
||||
/** 获取群会话 ID */
|
||||
export function getGroupConversationId(groupId) {
|
||||
return `GROUP${groupId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记会话为已读
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
*/
|
||||
export async function setMessageRead(targetUserId) {
|
||||
if (!chat || !isReady) return
|
||||
try {
|
||||
const conversationID = getConversationId(targetUserId)
|
||||
await chat.setMessageRead({ conversationID })
|
||||
} catch (e) {
|
||||
console.error('[IM] 标记已读失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(用于消息页展示聊天记录)
|
||||
* @returns {Promise<Array>} 会话列表
|
||||
*/
|
||||
/** 获取会话列表(群聊模式) */
|
||||
export async function getConversationList() {
|
||||
if (!chat || !isReady) return []
|
||||
try {
|
||||
const res = await chat.getConversationList()
|
||||
const list = res.data.conversationList || []
|
||||
// 只返回 C2C 单聊会话
|
||||
return list
|
||||
.filter(c => c.type === TencentCloudChat.TYPES.CONV_C2C)
|
||||
.filter(c => c.type === TencentCloudChat.TYPES.CONV_GROUP)
|
||||
.map(c => {
|
||||
const lastMsg = c.lastMessage
|
||||
let lastMessageText = ''
|
||||
|
|
@ -144,16 +85,14 @@ export async function getConversationList() {
|
|||
} else if (lastMsg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
||||
lastMessageText = '[图片]'
|
||||
} else if (lastMsg.type === TencentCloudChat.TYPES.MSG_CUSTOM) {
|
||||
lastMessageText = lastMsg.payload?.description || '[自定义消息]'
|
||||
lastMessageText = lastMsg.payload?.description || '[消息]'
|
||||
} else {
|
||||
lastMessageText = '[消息]'
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationID: c.conversationID,
|
||||
targetUserId: c.userProfile?.userID || c.conversationID.replace('C2C', ''),
|
||||
nickname: c.userProfile?.nick || c.conversationID.replace('C2C', ''),
|
||||
avatarUrl: c.userProfile?.avatar || '',
|
||||
groupId: c.groupProfile?.groupID || c.conversationID.replace('GROUP', ''),
|
||||
lastMessage: lastMessageText,
|
||||
lastMessageTime: lastMsg ? lastMsg.lastTime * 1000 : 0,
|
||||
unreadCount: c.unreadCount || 0
|
||||
|
|
@ -166,14 +105,9 @@ export async function getConversationList() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取历史消息
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
* @param {string} [nextReqMessageID] - 分页标记
|
||||
* @returns {Promise<{messageList, isCompleted, nextReqMessageID}>}
|
||||
*/
|
||||
export async function getMessageList(targetUserId, nextReqMessageID) {
|
||||
const conversationID = getConversationId(targetUserId)
|
||||
/** 拉取群历史消息 */
|
||||
export async function getMessageList(groupId, nextReqMessageID) {
|
||||
const conversationID = getGroupConversationId(groupId)
|
||||
const res = await chat.getMessageList({
|
||||
conversationID,
|
||||
nextReqMessageID,
|
||||
|
|
@ -186,173 +120,120 @@ export async function getMessageList(targetUserId, nextReqMessageID) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本消息
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
* @param {string} text - 文本内容
|
||||
* @param {string|number} [orderId] - 关联订单ID
|
||||
*/
|
||||
export async function sendTextMessage(targetUserId, text, orderId) {
|
||||
/** 标记群会话已读 */
|
||||
export async function setMessageRead(groupId) {
|
||||
if (!chat || !isReady) return
|
||||
try {
|
||||
const conversationID = getGroupConversationId(groupId)
|
||||
await chat.setMessageRead({ conversationID })
|
||||
} catch (e) {
|
||||
console.error('[IM] 标记已读失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送群文本消息 */
|
||||
export async function sendTextMessage(groupId, text) {
|
||||
const message = chat.createTextMessage({
|
||||
to: targetUserId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
to: groupId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_GROUP,
|
||||
payload: { text }
|
||||
})
|
||||
if (orderId) message.cloudCustomData = JSON.stringify({ orderId: String(orderId) })
|
||||
const res = await chat.sendMessage(message)
|
||||
return res.data.message
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送图片消息
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
* @param {Object} fileRes - uni.chooseImage 的 success 回调结果
|
||||
* @param {string|number} [orderId] - 关联订单ID
|
||||
*/
|
||||
export async function sendImageMessage(targetUserId, fileRes, orderId) {
|
||||
/** 发送群图片消息 */
|
||||
export async function sendImageMessage(groupId, fileRes) {
|
||||
const message = chat.createImageMessage({
|
||||
to: targetUserId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
to: groupId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_GROUP,
|
||||
payload: { file: fileRes }
|
||||
})
|
||||
if (orderId) message.cloudCustomData = JSON.stringify({ orderId: String(orderId) })
|
||||
const res = await chat.sendMessage(message)
|
||||
return res.data.message
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送自定义消息(改价申请等)
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
* @param {Object} customData - 自定义数据
|
||||
* @param {string|number} [orderId] - 关联订单ID
|
||||
*/
|
||||
export async function sendCustomMessage(targetUserId, customData, orderId) {
|
||||
/** 发送群自定义消息 */
|
||||
export async function sendCustomMessage(groupId, customData) {
|
||||
const message = chat.createCustomMessage({
|
||||
to: targetUserId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
to: groupId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_GROUP,
|
||||
payload: {
|
||||
data: JSON.stringify(customData),
|
||||
description: customData.description || '',
|
||||
extension: customData.extension || ''
|
||||
}
|
||||
})
|
||||
if (orderId) message.cloudCustomData = JSON.stringify({ orderId: String(orderId) })
|
||||
const res = await chat.sendMessage(message)
|
||||
return res.data.message
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 IM 消息转换为聊天页展示格式
|
||||
* @param {Object} msg - IM SDK 消息对象
|
||||
* @param {string} currentImUserId - 当前用户 IM ID
|
||||
* @returns {Object} 聊天页消息格式
|
||||
*/
|
||||
/** 将 IM 消息转换为聊天页展示格式 */
|
||||
export function formatIMMessage(msg, currentImUserId) {
|
||||
const isSelf = msg.from === currentImUserId
|
||||
const avatar = msg.avatar || '/static/logo.png'
|
||||
|
||||
// 提取消息关联的订单ID
|
||||
let msgOrderId = null
|
||||
if (msg.cloudCustomData) {
|
||||
try {
|
||||
const cd = JSON.parse(msg.cloudCustomData)
|
||||
msgOrderId = cd.orderId || null
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 文本消息
|
||||
if (msg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'text',
|
||||
content: msg.payload.text,
|
||||
isSelf,
|
||||
avatar,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
id: msg.ID, type: 'text', content: msg.payload.text,
|
||||
isSelf, avatar, time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
|
||||
// 图片消息
|
||||
if (msg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
||||
const imgArr = msg.payload.imageInfoArray || []
|
||||
// [0]原图 [1]大图 [2]缩略图,显示用大图,预览用原图
|
||||
const displayUrl = imgArr[1]?.url || imgArr[0]?.url || ''
|
||||
const originUrl = imgArr[0]?.url || imgArr[1]?.url || ''
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'image',
|
||||
content: displayUrl,
|
||||
originUrl: originUrl,
|
||||
isSelf,
|
||||
avatar,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
id: msg.ID, type: 'image', content: displayUrl, originUrl,
|
||||
isSelf, avatar, time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义消息(改价、订单状态等)
|
||||
// 自定义消息
|
||||
if (msg.type === TencentCloudChat.TYPES.MSG_CUSTOM) {
|
||||
try {
|
||||
const data = JSON.parse(msg.payload.data)
|
||||
if (data.bizType === 'price-change') {
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'price-change',
|
||||
isSelf,
|
||||
id: msg.ID, type: 'price-change', isSelf,
|
||||
priceChangeId: data.priceChangeId,
|
||||
changeTypeLabel: data.changeTypeLabel,
|
||||
originalPrice: data.originalPrice,
|
||||
newPrice: data.newPrice,
|
||||
difference: data.difference,
|
||||
status: data.status,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
if (data.bizType === 'price-change-response') {
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'price-change-response',
|
||||
id: msg.ID, type: 'price-change-response',
|
||||
priceChangeId: data.priceChangeId,
|
||||
action: data.action,
|
||||
status: data.status,
|
||||
action: data.action, status: data.status,
|
||||
changeTypeLabel: data.changeTypeLabel,
|
||||
content: data.description || '',
|
||||
isSelf,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
isSelf, time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
if (data.bizType === 'order-status') {
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'system',
|
||||
id: msg.ID, type: 'system',
|
||||
content: data.description || '[订单状态变更]',
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'text',
|
||||
content: msg.payload.description || '[自定义消息]',
|
||||
isSelf,
|
||||
avatar,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
id: msg.ID, type: 'text',
|
||||
content: msg.payload.description || '[消息]',
|
||||
isSelf, avatar, time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
|
||||
// 其他类型
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'text',
|
||||
content: '[暂不支持的消息类型]',
|
||||
isSelf,
|
||||
avatar,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
id: msg.ID, type: 'text', content: '[暂不支持的消息类型]',
|
||||
isSelf, avatar, time: msg.time * 1000
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,9 +66,17 @@ function request(options) {
|
|||
return
|
||||
}
|
||||
|
||||
// 未登录或 token 过期,静默清除凭证,不自动跳转
|
||||
// 未登录或 token 过期
|
||||
if (statusCode === 401) {
|
||||
// 清除过期的凭证
|
||||
clearAuth()
|
||||
// 非静默请求时提示用户重新登录
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const isTabPage = ['pages/index/index', 'pages/order-hall/order-hall', 'pages/message/message', 'pages/mine/mine'].includes(currentPage?.route)
|
||||
if (!isTabPage) {
|
||||
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
|
||||
}
|
||||
reject(new Error('未登录或登录已过期'))
|
||||
return
|
||||
}
|
||||
|
|
@ -114,7 +122,6 @@ function uploadFile(filePath, url = '/api/upload/image') {
|
|||
header,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 401) {
|
||||
clearAuth()
|
||||
reject(new Error('未登录或登录已过期'))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,15 +478,25 @@ public static class AdminEndpoints
|
|||
return Results.BadRequest(new { code = 400, message = "请填写取消原因" });
|
||||
|
||||
// 更新订单状态
|
||||
var wasPending = order.Status == OrderStatus.Pending || order.Status == OrderStatus.InProgress
|
||||
|| order.Status == OrderStatus.WaitConfirm || order.Status == OrderStatus.Appealing;
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// 发起微信退款(原路返回)
|
||||
// 只对已支付的订单发起退款(Unpaid 状态无需退款)
|
||||
var refundResult = false;
|
||||
if (wasPending)
|
||||
{
|
||||
var totalFen = (int)(order.TotalAmount * 100);
|
||||
var refundNo = $"R{order.OrderNo}";
|
||||
var refundResult = await wxPay.Refund(order.OrderNo, refundNo, totalFen, totalFen, request.Reason);
|
||||
|
||||
refundResult = await wxPay.Refund(order.OrderNo, refundNo, totalFen, totalFen, request.Reason);
|
||||
Console.WriteLine($"[管理端] 取消订单 {order.OrderNo},原因:{request.Reason},退款:{(refundResult ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[管理端] 取消订单 {order.OrderNo}(未支付,无需退款),原因:{request.Reason}");
|
||||
refundResult = true; // 未支付不需要退款,视为成功
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
|
|
@ -626,16 +636,26 @@ public static class AdminEndpoints
|
|||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端拉取聊天记录
|
||||
app.MapGet("/api/admin/chat-messages", async (int ownerUserId, int runnerUserId, TencentIMService imService) =>
|
||||
app.MapGet("/api/admin/chat-messages", async (string? groupId, int? ownerUserId, int? runnerUserId, TencentIMService imService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 优先用群ID拉取(群聊模式)
|
||||
if (!string.IsNullOrEmpty(groupId))
|
||||
{
|
||||
var result = await imService.GetGroupMessagesAsync(groupId);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
// 兼容旧数据:用C2C拉取
|
||||
if (ownerUserId.HasValue && runnerUserId.HasValue)
|
||||
{
|
||||
var fromImId = $"user_{ownerUserId}";
|
||||
var toImId = $"user_{runnerUserId}";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await imService.GetRoamMessagesAsync(fromImId, toImId);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
return Results.BadRequest(new { code = 400, message = "请提供 groupId 或 ownerUserId+runnerUserId" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { code = 400, message = $"拉取聊天记录失败: {ex.Message}" });
|
||||
|
|
|
|||
|
|
@ -133,6 +133,34 @@ public static class AuthEndpoints
|
|||
app.MapGet("/api/protected", () => Results.Ok("ok"))
|
||||
.RequireAuthorization();
|
||||
|
||||
// 更新用户信息(昵称、头像)
|
||||
app.MapPut("/api/user/profile", async (UpdateProfileRequest request, HttpContext httpContext, AppDbContext db) =>
|
||||
{
|
||||
var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
if (userIdClaim == null) return Results.Unauthorized();
|
||||
var userId = int.Parse(userIdClaim.Value);
|
||||
|
||||
var user = await db.Users.FindAsync(userId);
|
||||
if (user == null) return Results.NotFound(new { code = 404, message = "用户不存在" });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Nickname))
|
||||
user.Nickname = request.Nickname.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(request.AvatarUrl))
|
||||
user.AvatarUrl = request.AvatarUrl.Trim();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new UserInfo
|
||||
{
|
||||
Id = user.Id,
|
||||
Uid = user.Uid,
|
||||
Phone = user.Phone,
|
||||
Nickname = user.Nickname,
|
||||
AvatarUrl = user.AvatarUrl,
|
||||
Role = user.Role.ToString()
|
||||
});
|
||||
}).RequireAuthorization();
|
||||
|
||||
// 管理员账号密码登录接口
|
||||
app.MapPost("/api/admin/auth/login", async (
|
||||
AdminLoginRequest request,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using CampusErrand.Data;
|
|||
using CampusErrand.Models;
|
||||
using CampusErrand.Models.Dtos;
|
||||
using CampusErrand.Helpers;
|
||||
using CampusErrand.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CampusErrand.Endpoints;
|
||||
|
|
@ -106,13 +107,10 @@ public static class EarningEndpoints
|
|||
if (request.Amount != Math.Round(request.Amount, 2))
|
||||
return Results.BadRequest(new { code = 400, message = "请输入正确的提现金额" });
|
||||
|
||||
// 收款方式校验
|
||||
if (!Enum.TryParse<PaymentMethod>(request.PaymentMethod, true, out var paymentMethod) || !Enum.IsDefined(paymentMethod))
|
||||
return Results.BadRequest(new { code = 400, message = "收款方式不合法" });
|
||||
|
||||
// 收款二维码校验
|
||||
if (string.IsNullOrWhiteSpace(request.QrCodeImage))
|
||||
return Results.BadRequest(new { code = 400, message = "收款二维码不能为空" });
|
||||
// 收款方式默认微信零钱
|
||||
var paymentMethod = PaymentMethod.WeChat;
|
||||
if (!string.IsNullOrEmpty(request.PaymentMethod))
|
||||
Enum.TryParse<PaymentMethod>(request.PaymentMethod, true, out paymentMethod);
|
||||
|
||||
// 先执行冻结解冻逻辑
|
||||
await BusinessHelpers.UnfreezeEarnings(db);
|
||||
|
|
@ -220,7 +218,7 @@ public static class EarningEndpoints
|
|||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端审核提现(通过/拒绝)
|
||||
app.MapPut("/api/admin/withdrawals/{id}", async (int id, AdminWithdrawalRequest request, AppDbContext db) =>
|
||||
app.MapPut("/api/admin/withdrawals/{id}", async (int id, AdminWithdrawalRequest request, AppDbContext db, WxPayService wxPay) =>
|
||||
{
|
||||
var withdrawal = await db.Withdrawals.FindAsync(id);
|
||||
if (withdrawal == null)
|
||||
|
|
@ -231,6 +229,22 @@ 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 (transferSuccess, transferError) = await wxPay.TransferToWallet(batchNo, detailNo, user.OpenId, amountFen, "跑腿提现到账");
|
||||
|
||||
if (!transferSuccess)
|
||||
{
|
||||
Console.WriteLine($"[提现] 转账失败: {transferError}");
|
||||
return Results.BadRequest(new { code = 400, message = $"转账失败,请稍后重试", detail = transferError });
|
||||
}
|
||||
|
||||
withdrawal.Status = WithdrawalStatus.Completed;
|
||||
withdrawal.ProcessedAt = DateTime.UtcNow;
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ public static class OrderEndpoints
|
|||
OwnerAvatar = o.Owner!.AvatarUrl,
|
||||
RunnerNickname = o.Runner!.Nickname,
|
||||
RunnerAvatar = o.Runner!.AvatarUrl,
|
||||
LastTime = o.CompletedAt ?? o.AcceptedAt ?? o.CreatedAt
|
||||
LastTime = o.CompletedAt ?? o.AcceptedAt ?? o.CreatedAt,
|
||||
o.ImGroupId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -208,7 +209,8 @@ public static class OrderEndpoints
|
|||
TargetUserId = targetId,
|
||||
TargetNickname = nickname,
|
||||
TargetAvatar = avatar ?? "",
|
||||
o.LastTime
|
||||
o.LastTime,
|
||||
o.ImGroupId
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -334,6 +336,8 @@ public static class OrderEndpoints
|
|||
RunnerPhone = runnerPhone
|
||||
};
|
||||
|
||||
response.ImGroupId = order.ImGroupId;
|
||||
|
||||
// 查询佣金抽成信息(有 Earning 记录用实际值,否则实时计算预估值)
|
||||
if (order.RunnerId.HasValue)
|
||||
{
|
||||
|
|
@ -455,7 +459,7 @@ public static class OrderEndpoints
|
|||
}).AllowAnonymous();
|
||||
|
||||
// 接单
|
||||
app.MapPost("/api/orders/{id}/accept", async (int id, HttpContext httpContext, AppDbContext db) =>
|
||||
app.MapPost("/api/orders/{id}/accept", async (int id, HttpContext httpContext, AppDbContext db, TencentIMService imService) =>
|
||||
{
|
||||
// 获取当前用户
|
||||
var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
|
|
@ -515,6 +519,24 @@ public static class OrderEndpoints
|
|||
return Results.Conflict(new { code = 409, message = "该订单已被接取" });
|
||||
}
|
||||
|
||||
// 创建 IM 群组
|
||||
try
|
||||
{
|
||||
var groupId = $"order_{order.Id}";
|
||||
var ownerImId = $"user_{order.OwnerId}";
|
||||
var runnerImId = $"user_{userId}";
|
||||
var result = await imService.CreateGroupAsync(groupId, $"订单{order.OrderNo}", ownerImId, runnerImId);
|
||||
if (result != null)
|
||||
{
|
||||
order.ImGroupId = result;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[IM] 创建群组失败: {ex.Message}");
|
||||
}
|
||||
|
||||
return Results.Ok(new AcceptOrderResponse
|
||||
{
|
||||
Id = order.Id,
|
||||
|
|
@ -1041,7 +1063,7 @@ public static class OrderEndpoints
|
|||
var refundAmount = Math.Abs(totalDiff);
|
||||
var refundFen = (int)(refundAmount * 100);
|
||||
var totalFen = (int)(oldTotal * 100);
|
||||
var refundNo = $"RP{order.OrderNo}{DateTime.UtcNow:mmss}";
|
||||
var refundNo = $"RP{order.OrderNo}{DateTime.UtcNow:HHmmss}{Random.Shared.Next(100, 999)}";
|
||||
refundSuccess = await wxPay.Refund(order.OrderNo, refundNo, totalFen, refundFen, "改价退款");
|
||||
Console.WriteLine($"[改价] 退款{(refundSuccess ? "成功" : "失败")}: ¥{refundAmount}");
|
||||
}
|
||||
|
|
|
|||
916
server/Migrations/20260401162116_AddOrderImGroupId.Designer.cs
generated
Normal file
916
server/Migrations/20260401162116_AddOrderImGroupId.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,916 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using CampusErrand.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CampusErrand.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260401162116_AddOrderImGroupId")]
|
||||
partial class AddOrderImGroupId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("OrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Result")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("Appeals");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Banner", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("LinkType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder");
|
||||
|
||||
b.ToTable("Banners");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.CommissionRule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal?>("MaxAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal>("MinAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal>("Rate")
|
||||
.HasColumnType("decimal(10,4)");
|
||||
|
||||
b.Property<string>("RateType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CommissionRules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Photo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<int>("ShopId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShopId");
|
||||
|
||||
b.ToTable("Dishes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("Commission")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("FrozenUntil")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("GoodsAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal>("NetEarning")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<int>("OrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("PlatformFee")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Earnings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DishId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("OrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ShopId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("UnitPrice")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DishId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("ShopId");
|
||||
|
||||
b.ToTable("FoodOrderItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("MessageId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTime>("ReadAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "MessageType", "MessageId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MessageReads");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Order", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("AcceptedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("Commission")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CompletionProof")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeliveryLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<decimal?>("GoodsAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<string>("ImGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsReviewed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("ItemName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("OrderNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("OrderType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("PickupLocation")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<int?>("RunnerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<decimal>("TotalAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderNo")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("RunnerId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("Orders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ChangeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("InitiatorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("NewPrice")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<int>("OrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("OriginalPrice")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InitiatorId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("PriceChanges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Review", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsDisabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("OrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RunnerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ScoreChange")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("RunnerId");
|
||||
|
||||
b.ToTable("Reviews");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("RealName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<DateTime?>("ReviewedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RunnerCertifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.ServiceEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("PagePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SortOrder");
|
||||
|
||||
b.ToTable("ServiceEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Notice")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<decimal>("PackingFeeAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<string>("PackingFeeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Photo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Shops");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<int>("ShopId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShopId");
|
||||
|
||||
b.ToTable("ShopBanners");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.SystemConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SystemConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.SystemMessage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("TargetType")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TargetUserIds")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ThumbnailUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SystemMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("OpenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("RunnerScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OpenId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Phone");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("ProcessedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QrCodeImage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Withdrawals");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany("Dishes")
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Dish", "Dish")
|
||||
.WithMany()
|
||||
.HasForeignKey("DishId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany("FoodOrderItems")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany()
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dish");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Order", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "Runner")
|
||||
.WithMany()
|
||||
.HasForeignKey("RunnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Runner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "Initiator")
|
||||
.WithMany()
|
||||
.HasForeignKey("InitiatorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Initiator");
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Review", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "Runner")
|
||||
.WithMany()
|
||||
.HasForeignKey("RunnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("Runner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany("ShopBanners")
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Order", b =>
|
||||
{
|
||||
b.Navigation("FoodOrderItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
|
||||
{
|
||||
b.Navigation("Dishes");
|
||||
|
||||
b.Navigation("ShopBanners");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
29
server/Migrations/20260401162116_AddOrderImGroupId.cs
Normal file
29
server/Migrations/20260401162116_AddOrderImGroupId.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CampusErrand.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOrderImGroupId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ImGroupId",
|
||||
table: "Orders",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ImGroupId",
|
||||
table: "Orders");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -286,6 +286,10 @@ namespace CampusErrand.Migrations
|
|||
b.Property<decimal?>("GoodsAmount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<string>("ImGroupId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsReviewed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
|
|
|||
|
|
@ -68,3 +68,14 @@ public class AdminLoginRequest
|
|||
[Required(ErrorMessage = "密码不能为空")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户信息请求
|
||||
/// </summary>
|
||||
public class UpdateProfileRequest
|
||||
{
|
||||
/// <summary>昵称</summary>
|
||||
public string? Nickname { get; set; }
|
||||
/// <summary>头像URL</summary>
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ public class OrderResponse
|
|||
public decimal? PlatformFee { get; set; }
|
||||
/// <summary>跑腿实得佣金(订单完成后可见)</summary>
|
||||
public decimal? NetEarning { get; set; }
|
||||
/// <summary>IM 群组 ID</summary>
|
||||
public string? ImGroupId { get; set; }
|
||||
public List<FoodOrderItemResponse>? FoodItems { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ public class Order
|
|||
/// <summary>完成时间</summary>
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>IM 群组 ID(接单时创建)</summary>
|
||||
[MaxLength(64)]
|
||||
public string? ImGroupId { get; set; }
|
||||
|
||||
// 导航属性
|
||||
[ForeignKey(nameof(OwnerId))]
|
||||
public User? Owner { get; set; }
|
||||
|
|
|
|||
|
|
@ -82,4 +82,65 @@ public class TencentIMService
|
|||
Console.WriteLine($"[IM] 拉取漫游消息: {fromUserId} -> {toUserId}, 响应: {json}");
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 IM 群组(每个订单一个群)
|
||||
/// </summary>
|
||||
public async Task<string?> CreateGroupAsync(string groupId, string groupName, string ownerImId, string runnerImId)
|
||||
{
|
||||
var adminSig = GenerateUserSig(AdminAccount);
|
||||
var random = Random.Shared.Next(100000, 999999);
|
||||
var url = $"https://console.tim.qq.com/v4/group_open_http_svc/create_group?sdkappid={_sdkAppId}&identifier={AdminAccount}&usersig={adminSig}&random={random}&contenttype=json";
|
||||
|
||||
var body = new
|
||||
{
|
||||
Type = "Private",
|
||||
GroupId = groupId,
|
||||
Name = groupName,
|
||||
Owner_Account = ownerImId,
|
||||
MemberList = new[]
|
||||
{
|
||||
new { Member_Account = runnerImId }
|
||||
}
|
||||
};
|
||||
|
||||
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}");
|
||||
|
||||
var result = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
var actionStatus = result.GetProperty("ActionStatus").GetString();
|
||||
if (actionStatus == "OK")
|
||||
return result.GetProperty("GroupId").GetString();
|
||||
|
||||
// 群已存在也算成功
|
||||
var errorCode = result.GetProperty("ErrorCode").GetInt32();
|
||||
if (errorCode == 10021)
|
||||
return groupId;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拉取群漫游消息(管理后台用)
|
||||
/// </summary>
|
||||
public async Task<JsonElement> GetGroupMessagesAsync(string groupId, int reqMsgSeq = 0, int reqMsgNumber = 20)
|
||||
{
|
||||
var adminSig = GenerateUserSig(AdminAccount);
|
||||
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
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(url, content);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,60 @@ public class WxPayService
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商家转账到零钱(提现用)
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? ErrorMessage)> TransferToWallet(string outBatchNo, string outDetailNo, string openId, int amountFen, string remark = "提现到账")
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
appid = _appId,
|
||||
out_batch_no = outBatchNo,
|
||||
batch_name = "跑腿提现",
|
||||
batch_remark = remark,
|
||||
total_amount = amountFen,
|
||||
total_num = 1,
|
||||
transfer_detail_list = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
out_detail_no = outDetailNo,
|
||||
transfer_amount = amountFen,
|
||||
transfer_remark = remark,
|
||||
openid = openId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(requestBody);
|
||||
var url = "/v3/transfer/batches";
|
||||
var fullUrl = $"https://api.mch.weixin.qq.com{url}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, fullUrl)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
var nonce = Guid.NewGuid().ToString("N");
|
||||
var signature = Sign("POST", url, timestamp, nonce, json);
|
||||
request.Headers.Add("Authorization", $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_mchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"");
|
||||
request.Headers.Add("Accept", "application/json");
|
||||
request.Headers.Add("User-Agent", "CampusErrand/1.0");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"[微信转账] 转账失败: {responseBody}");
|
||||
return (false, responseBody);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[微信转账] 转账成功: {outBatchNo}");
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成签名
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user