提现
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-04-02 16:55:18 +08:00
parent b359070a0e
commit 681d2b5fe8
23 changed files with 235 additions and 107 deletions

View File

@ -1,3 +1,4 @@
{
"git.ignoreLimitWarning": true
"git.ignoreLimitWarning": true,
"kiro.chat.autoSummarize.threshold": 30
}

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -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) {}
}

View File

@ -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: {
/** 加载未读消息数 */

View File

@ -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())}`
}

View File

@ -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())}`
}

View File

@ -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())}`
}

View File

@ -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' })

View File

@ -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
}
}
})
},
/** 昵称输入框失焦(微信昵称选择后触发) */

View File

@ -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')

View File

@ -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())}`
},

View File

@ -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())}`
},

View File

@ -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())}`
},

View File

@ -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) {

View File

@ -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");
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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}");
}
}