跑腿,聊天
This commit is contained in:
parent
58b34666bd
commit
f9b90a761a
|
|
@ -1,47 +1,85 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3 style="margin: 0 0 16px;">发布系统通知</h3>
|
||||
<div class="notifications-page">
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 发布通知 -->
|
||||
<el-tab-pane label="发布通知" name="publish">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="max-width: 700px;">
|
||||
<el-form-item label="通知标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入通知标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通知正文" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :rows="8" placeholder="支持富文本内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缩略图">
|
||||
<el-input v-model="form.thumbnailUrl" placeholder="缩略图地址(选填)" />
|
||||
<el-upload action="/api/upload/image" :headers="uploadHeaders" :show-file-list="false"
|
||||
:on-success="(res) => form.thumbnailUrl = res.url" accept="image/*" style="margin-top: 8px;">
|
||||
<el-button size="small">上传图片</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标用户" prop="targetType">
|
||||
<el-select v-model="form.targetType" style="width: 100%;">
|
||||
<el-option label="全部用户" value="All" />
|
||||
<el-option label="下单用户" value="OrderUser" />
|
||||
<el-option label="跑腿用户" value="RunnerUser" />
|
||||
<el-option label="指定用户" value="Specific" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.targetType === 'Specific'" label="用户ID列表">
|
||||
<el-input v-model="targetUserIdsText" placeholder="多个用户ID用逗号分隔,如:1,2,3" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">发布通知</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="max-width: 700px;">
|
||||
<el-form-item label="通知标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入通知标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通知正文" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :rows="8" placeholder="支持富文本内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缩略图">
|
||||
<el-input v-model="form.thumbnailUrl" placeholder="缩略图地址(选填)" />
|
||||
<el-upload action="/api/upload/image" :headers="uploadHeaders" :show-file-list="false"
|
||||
:on-success="(res) => form.thumbnailUrl = res.url" accept="image/*" style="margin-top: 8px;">
|
||||
<el-button size="small">上传图片</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标用户" prop="targetType">
|
||||
<el-select v-model="form.targetType" style="width: 100%;">
|
||||
<el-option label="全部用户" value="All" />
|
||||
<el-option label="下单用户" value="OrderUser" />
|
||||
<el-option label="跑腿用户" value="RunnerUser" />
|
||||
<el-option label="指定用户" value="Specific" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.targetType === 'Specific'" label="用户ID列表">
|
||||
<el-input v-model="targetUserIdsText" placeholder="多个用户ID用逗号分隔,如:1,2,3" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">发布通知</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 已发送通知 -->
|
||||
<el-tab-pane label="已发送" name="list">
|
||||
<el-table :data="notifyList" v-loading="listLoading" stripe>
|
||||
<el-table-column prop="id" label="ID" width="60" align="center" />
|
||||
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="contentPreview" label="内容预览" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="目标" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="targetTagType(row.targetType)">{{ targetLabel(row.targetType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="缩略图" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.thumbnailUrl" :src="row.thumbnailUrl" style="width: 40px; height: 40px;" fit="cover" :preview-src-list="[row.thumbnailUrl]" />
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm title="删除后用户端也会同步删除,确定?" @confirm="deleteNotify(row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger" plain>删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const activeTab = ref('publish')
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
const targetUserIdsText = ref('')
|
||||
const listLoading = ref(false)
|
||||
const notifyList = ref([])
|
||||
|
||||
const uploadHeaders = { Authorization: `Bearer ${localStorage.getItem('admin_token')}` }
|
||||
|
||||
|
|
@ -58,10 +96,13 @@ const rules = {
|
|||
targetType: [{ required: true, message: '请选择目标用户', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const targetLabel = (t) => ({ All: '全部', OrderUser: '下单用户', RunnerUser: '跑腿用户', Specific: '指定用户' }[t] || t)
|
||||
const targetTagType = (t) => ({ All: '', OrderUser: 'success', RunnerUser: 'warning', Specific: 'info' }[t] || '')
|
||||
const formatTime = (t) => t ? new Date(t).toLocaleString('zh-CN') : ''
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
const data = { ...form }
|
||||
if (data.targetType === 'Specific') {
|
||||
data.targetUserIds = targetUserIdsText.value.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
|
||||
|
|
@ -70,16 +111,38 @@ async function handleSubmit() {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await request.post('/admin/notifications', data)
|
||||
ElMessage.success('发布成功')
|
||||
// 重置表单
|
||||
formRef.value.resetFields()
|
||||
targetUserIdsText.value = ''
|
||||
fetchList()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
notifyList.value = await request.get('/admin/notifications')
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotify(row) {
|
||||
await request.delete(`/admin/notifications/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
fetchList()
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notifications-page {
|
||||
max-width: 900px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@
|
|||
<div class="runners-page">
|
||||
<div class="page-header">
|
||||
<h2>跑腿管理</h2>
|
||||
<el-tag type="info" size="large">共 {{ list.length }} 名跑腿</el-tag>
|
||||
<div class="header-actions">
|
||||
<el-input v-model="searchUid" placeholder="输入UID搜索" clearable style="width: 200px;"
|
||||
@clear="searchUid = ''" @keyup.enter="searchUid = searchUid.trim()">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-tag type="info" size="large">共 {{ filteredList.length }} 名跑腿</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table :data="filteredList" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
|
|
@ -25,10 +31,9 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="评价星级" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.reviewCount > 0" style="color: #ff9900; font-weight: bold;">
|
||||
★ {{ row.avgRating }}
|
||||
<span style="color: #ff9900; font-weight: bold;">
|
||||
★ {{ row.starRating }}
|
||||
</span>
|
||||
<span v-else style="color: #ccc;">暂无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="被评价" width="100" align="center">
|
||||
|
|
@ -107,12 +112,19 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '../utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const searchUid = ref('')
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!searchUid.value) return list.value
|
||||
return list.value.filter(r => String(r.id).includes(searchUid.value.trim()))
|
||||
})
|
||||
const reviewDialogVisible = ref(false)
|
||||
const reviewLoading = ref(false)
|
||||
const reviews = ref([])
|
||||
|
|
@ -177,6 +189,11 @@ onMounted(fetchList)
|
|||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.score-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
9
miniapp/package-lock.json
generated
9
miniapp/package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.6.6",
|
||||
"pinia": "^3.0.4"
|
||||
"pinia": "^3.0.4",
|
||||
"tim-upload-plugin": "^1.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fast-check": "^4.5.3",
|
||||
|
|
@ -1599,6 +1600,12 @@
|
|||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tim-upload-plugin": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tim-upload-plugin/-/tim-upload-plugin-1.4.3.tgz",
|
||||
"integrity": "sha512-3ZmbA36dr3eG9YGDon9MLBUtbNawYWkL+TBa+VS0Uviguc7PlVSOIVRG2C4irXX16slDT2Kj+HAZapp+Xqp2xg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.6.6",
|
||||
"pinia": "^3.0.4"
|
||||
"pinia": "^3.0.4",
|
||||
"tim-upload-plugin": "^1.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fast-check": "^4.5.3",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { createOrder, getPageBanner, getMinCommission } from '../../utils/api'
|
||||
import { createOrder, getPageBanner, getMinCommission, cancelOrder, confirmPayment } from '../../utils/api'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -139,8 +139,8 @@ export default {
|
|||
if (isNaN(num) || num < this.minCommission) {
|
||||
uni.showToast({ title: `跑腿佣金不可低于${this.minCommission}元`, icon: 'none' }); return false
|
||||
}
|
||||
if (val.includes('.') && val.split('.')[1].length > 1) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后1位', icon: 'none' }); return false
|
||||
if (val.includes('.') && val.split('.')[1].length > 2) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后2位', icon: 'none' }); return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
|
@ -184,7 +184,17 @@ export default {
|
|||
commission,
|
||||
totalAmount: commission
|
||||
})
|
||||
if (result.paymentParams) await this.wxPay(result.paymentParams)
|
||||
if (result.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(result.paymentParams)
|
||||
// 支付成功,确认订单上架
|
||||
try { await confirmPayment(result.id) } catch (ex) {}
|
||||
} catch (e) {
|
||||
// 支付失败/取消,撤销订单
|
||||
try { await cancelOrder(result.id) } catch (ex) {}
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.showToast({ title: '下单成功', icon: 'success' })
|
||||
setTimeout(() => { uni.navigateBack() }, 1500)
|
||||
} catch (e) {} finally { this.submitting = false }
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { createOrder, getMinCommission } from '../../utils/api'
|
||||
import { createOrder, getMinCommission, cancelOrder, confirmPayment } from '../../utils/api'
|
||||
import { useCartStore } from '../../stores/cart'
|
||||
|
||||
export default {
|
||||
|
|
@ -133,8 +133,8 @@ export default {
|
|||
uni.showToast({ title: `跑腿佣金不可低于${this.minCommission}元`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (val.includes('.') && val.split('.')[1].length > 1) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后1位', icon: 'none' })
|
||||
if (val.includes('.') && val.split('.')[1].length > 2) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后2位', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -161,6 +161,13 @@ export default {
|
|||
async onSubmit() {
|
||||
if (!this.validateForm()) return
|
||||
|
||||
// 未登录跳转登录页
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
try {
|
||||
const commission = parseFloat(this.form.commission)
|
||||
|
|
@ -183,7 +190,15 @@ export default {
|
|||
const result = await createOrder(orderData)
|
||||
|
||||
if (result.paymentParams) {
|
||||
await this.wxPay(result.paymentParams)
|
||||
try {
|
||||
await this.wxPay(result.paymentParams)
|
||||
// 支付成功,确认订单上架
|
||||
try { await confirmPayment(result.id) } catch (ex) {}
|
||||
} catch (e) {
|
||||
// 支付失败/取消,撤销订单
|
||||
try { await cancelOrder(result.id) } catch (ex) {}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 下单成功,清空购物车
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@
|
|||
import {
|
||||
createOrder,
|
||||
getPageBanner,
|
||||
getMinCommission
|
||||
getMinCommission,
|
||||
cancelOrder,
|
||||
confirmPayment
|
||||
} from '../../utils/api'
|
||||
|
||||
export default {
|
||||
|
|
@ -149,9 +151,9 @@
|
|||
})
|
||||
return false
|
||||
}
|
||||
if (val.includes('.') && val.split('.')[1].length > 1) {
|
||||
if (val.includes('.') && val.split('.')[1].length > 2) {
|
||||
uni.showToast({
|
||||
title: '佣金最多支持小数点后1位',
|
||||
title: '佣金最多支持小数点后2位',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
|
|
@ -181,15 +183,11 @@
|
|||
})
|
||||
return false
|
||||
}
|
||||
if (!this.form.goodsAmount) {
|
||||
uni.showToast({
|
||||
title: '请输入商品总金额',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
if (!this.form.goodsAmount && this.form.goodsAmount !== '0') {
|
||||
this.form.goodsAmount = '0'
|
||||
}
|
||||
const goodsNum = parseFloat(this.form.goodsAmount)
|
||||
if (isNaN(goodsNum) || goodsNum <= 0) {
|
||||
if (isNaN(goodsNum) || goodsNum < 0) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的商品总金额',
|
||||
icon: 'none'
|
||||
|
|
@ -226,7 +224,17 @@
|
|||
commission,
|
||||
totalAmount: goodsAmount + commission
|
||||
})
|
||||
if (result.paymentParams) await this.wxPay(result.paymentParams)
|
||||
if (result.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(result.paymentParams)
|
||||
// 支付成功,确认订单上架
|
||||
try { await confirmPayment(result.id) } catch (ex) {}
|
||||
} catch (e) {
|
||||
// 支付失败/取消,撤销订单
|
||||
try { await cancelOrder(result.id) } catch (ex) {}
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.showToast({
|
||||
title: '下单成功',
|
||||
icon: 'success'
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@
|
|||
<view class="service-list">
|
||||
<view class="service-card" v-for="entry in serviceEntries" :key="entry.id" @click="onServiceClick(entry)">
|
||||
<image class="service-bg" :src="entry.iconUrl" mode="aspectFit"></image>
|
||||
<view class="service-overlay">
|
||||
<text class="service-name">{{ entry.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -151,22 +148,4 @@
|
|||
left: 0;
|
||||
}
|
||||
|
||||
.service-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -133,6 +133,9 @@
|
|||
<view class="quick-btn" @click="onContactService">
|
||||
<text>联系客服</text>
|
||||
</view>
|
||||
<view class="quick-btn btn-pay" v-if="pendingPayment" @click="retryPayment">
|
||||
<text>补缴支付</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部输入区域 -->
|
||||
|
|
@ -239,7 +242,8 @@
|
|||
priceChangeType: '',
|
||||
newPriceInput: '',
|
||||
submittingPriceChange: false,
|
||||
statusBarHeight: 0
|
||||
statusBarHeight: 0,
|
||||
pendingPayment: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -283,9 +287,8 @@
|
|||
offNewMessage()
|
||||
},
|
||||
onShow() {
|
||||
if (this.orderId && !this.orderInfo.id) {
|
||||
this.loadOrderInfo()
|
||||
} else if (this.orderId && this.lastOrderStatus) {
|
||||
// 每次回到聊天页都检测订单状态变化
|
||||
if (this.orderId) {
|
||||
this.checkOrderStatusChange()
|
||||
}
|
||||
},
|
||||
|
|
@ -339,7 +342,20 @@
|
|||
const formatted = formatIMMessage(msg, this.imUserId)
|
||||
// 按订单ID过滤
|
||||
if (this.orderId && formatted.orderId && String(formatted.orderId) !== String(this.orderId)) continue
|
||||
this.chatMessages.push(formatted)
|
||||
// 收到改价响应消息,更新对应改价卡片状态
|
||||
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',
|
||||
content: `对方${formatted.action === 'Accepted' ? '同意' : '拒绝'}了${formatted.changeTypeLabel}改价`
|
||||
})
|
||||
if (formatted.action === 'Accepted') this.loadOrderInfo()
|
||||
} else {
|
||||
this.chatMessages.push(formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.scrollToBottom()
|
||||
|
|
@ -360,9 +376,20 @@
|
|||
const res = await getMessageList(this.targetImUserId, this.nextReqMessageID)
|
||||
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId))
|
||||
// 按订单ID过滤:只显示当前订单的消息,或没有orderId的旧消息
|
||||
const formatted = this.orderId
|
||||
const filtered = this.orderId
|
||||
? allFormatted.filter(m => !m.orderId || String(m.orderId) === String(this.orderId))
|
||||
: allFormatted
|
||||
// 处理历史消息中的改价响应,更新对应改价卡片状态
|
||||
const formatted = []
|
||||
for (const m of filtered) {
|
||||
if (m.type === 'price-change-response') {
|
||||
const card = filtered.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 {
|
||||
formatted.push(m)
|
||||
}
|
||||
}
|
||||
this.chatMessages = [...formatted, ...this.chatMessages]
|
||||
this.nextReqMessageID = res.nextReqMessageID
|
||||
this.historyCompleted = res.isCompleted
|
||||
|
|
@ -422,7 +449,7 @@
|
|||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
try {
|
||||
const msg = await sendImageMessage(this.targetImUserId, res.tempFilePaths[0], this.orderId)
|
||||
const msg = await sendImageMessage(this.targetImUserId, res, this.orderId)
|
||||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||||
this.scrollToBottom()
|
||||
} catch (e) {
|
||||
|
|
@ -435,17 +462,28 @@
|
|||
})
|
||||
},
|
||||
onChangeCommission() {
|
||||
this.showMorePanel = false;
|
||||
this.showMorePanel = false
|
||||
if (this.hasPendingPriceChange()) return
|
||||
this.priceChangeType = 'Commission'
|
||||
this.newPriceInput = '';
|
||||
this.newPriceInput = ''
|
||||
this.showPriceChangePopup = true
|
||||
},
|
||||
onChangeGoodsPrice() {
|
||||
this.showMorePanel = false;
|
||||
this.showMorePanel = false
|
||||
if (this.hasPendingPriceChange()) return
|
||||
this.priceChangeType = 'GoodsAmount'
|
||||
this.newPriceInput = '';
|
||||
this.newPriceInput = ''
|
||||
this.showPriceChangePopup = true
|
||||
},
|
||||
/** 检查是否有待处理的改价申请 */
|
||||
hasPendingPriceChange() {
|
||||
const pending = this.chatMessages.find(m => m.type === 'price-change' && m.status === 'Pending')
|
||||
if (pending) {
|
||||
uni.showToast({ title: '已有等待处理的改价申请,无法同时申请', icon: 'none' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
async submitPriceChange() {
|
||||
const newPrice = parseFloat(this.newPriceInput)
|
||||
if (isNaN(newPrice) || newPrice < 0) {
|
||||
|
|
@ -529,12 +567,47 @@
|
|||
type: 'system',
|
||||
content: `您已${actionLabel}${changeTypeLabel}改价`
|
||||
})
|
||||
if (action === 'Accepted' && res.difference !== 0) {
|
||||
if (res.difference < 0) this.chatMessages.push({
|
||||
id: `sys_r_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `已退还您${Math.abs(res.difference).toFixed(2)}元`
|
||||
})
|
||||
|
||||
// 通过 IM 通知对方改价结果
|
||||
if (this.imReady) {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'price-change-response',
|
||||
priceChangeId: priceChangeId,
|
||||
action: action,
|
||||
status: res.status,
|
||||
changeTypeLabel,
|
||||
description: `${actionLabel}了${changeTypeLabel}改价申请`
|
||||
}, this.orderId)
|
||||
}
|
||||
|
||||
if (action === 'Accepted') {
|
||||
// 需要补价:拉起支付
|
||||
if (res.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(res.paymentParams)
|
||||
this.chatMessages.push({
|
||||
id: `sys_pay_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `已补缴¥${res.difference.toFixed(2)}`
|
||||
})
|
||||
} catch (e) {
|
||||
// 支付取消或失败,保存待支付信息供再次支付
|
||||
this.pendingPayment = res.paymentParams
|
||||
this.chatMessages.push({
|
||||
id: `sys_pay_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: `需补缴¥${res.difference.toFixed(2)},点击下方"补缴支付"完成支付`
|
||||
})
|
||||
}
|
||||
}
|
||||
// 需要退款:后端已自动退款
|
||||
if (res.difference < 0) {
|
||||
this.chatMessages.push({
|
||||
id: `sys_r_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: res.refundSuccess ? `已退还您¥${Math.abs(res.difference).toFixed(2)}` : '退款处理中,请稍后查看'
|
||||
})
|
||||
}
|
||||
await this.loadOrderInfo()
|
||||
}
|
||||
this.scrollToBottom()
|
||||
|
|
@ -553,11 +626,16 @@
|
|||
return
|
||||
}
|
||||
const newStatus = this.orderInfo.status
|
||||
if (!oldStatus || oldStatus === newStatus) return
|
||||
// 首次加载只记录状态,不提示
|
||||
if (!oldStatus) {
|
||||
this.lastOrderStatus = newStatus
|
||||
return
|
||||
}
|
||||
if (oldStatus === newStatus) return
|
||||
this.lastOrderStatus = newStatus
|
||||
|
||||
const statusMessages = {
|
||||
'InProgress→WaitConfirm': '跑腿已提交完成,等待单主确认',
|
||||
'InProgress→WaitConfirm': '跑腿已提交完成,请在订单详情中确认',
|
||||
'WaitConfirm→Completed': '单主已确认,订单已完成',
|
||||
'WaitConfirm→InProgress': '单主已拒绝完成,订单继续进行'
|
||||
}
|
||||
|
|
@ -565,24 +643,16 @@
|
|||
const msg = statusMessages[key]
|
||||
if (!msg) return
|
||||
|
||||
// 避免重复:检查最后一条系统消息是否相同
|
||||
const lastMsg = [...this.chatMessages].reverse().find(m => m.type === 'system')
|
||||
if (lastMsg && lastMsg.content === msg) return
|
||||
|
||||
this.chatMessages.push({
|
||||
id: `sys_status_${Date.now()}`,
|
||||
type: 'system',
|
||||
content: msg
|
||||
})
|
||||
this.scrollToBottom()
|
||||
|
||||
if (this.imReady && this.targetImUserId) {
|
||||
try {
|
||||
await sendCustomMessage(this.targetImUserId, {
|
||||
bizType: 'order-status',
|
||||
action: key,
|
||||
description: msg
|
||||
}, this.orderId)
|
||||
} catch (e) {
|
||||
console.error('[聊天] 发送状态通知失败:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleteOrder() {
|
||||
this.showMorePanel = false
|
||||
|
|
@ -591,6 +661,14 @@
|
|||
})
|
||||
},
|
||||
goOrderDetail() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length >= 2) {
|
||||
const prevPage = pages[pages.length - 2]
|
||||
if (prevPage.route === 'pages/order/order-detail') {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/order-detail?id=${this.orderId}`
|
||||
})
|
||||
|
|
@ -614,6 +692,36 @@
|
|||
url: '/pages/config/qrcode'
|
||||
})
|
||||
},
|
||||
/** 微信支付 */
|
||||
wxPay(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
timeStamp: params.timeStamp,
|
||||
nonceStr: params.nonceStr,
|
||||
package: params.package_,
|
||||
signType: params.signType,
|
||||
paySign: params.paySign,
|
||||
success: () => {
|
||||
this.pendingPayment = null
|
||||
resolve()
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg !== 'requestPayment:fail cancel')
|
||||
uni.showToast({ title: '支付失败', icon: 'none' })
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 重新补缴支付 */
|
||||
async retryPayment() {
|
||||
if (!this.pendingPayment) return
|
||||
try {
|
||||
await this.wxPay(this.pendingPayment)
|
||||
uni.showToast({ title: '补缴成功', icon: 'success' })
|
||||
} catch (e) {}
|
||||
},
|
||||
previewImage(url) {
|
||||
uni.previewImage({
|
||||
urls: [url]
|
||||
|
|
@ -887,6 +995,15 @@
|
|||
color: #333;
|
||||
}
|
||||
|
||||
.btn-pay {
|
||||
background: #FAD146;
|
||||
border-color: #FAD146;
|
||||
}
|
||||
|
||||
.btn-pay text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 底部输入区域 */
|
||||
.chat-bottom {
|
||||
background: #fff;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,12 @@
|
|||
:key="item.orderId"
|
||||
@click="goChat(item)"
|
||||
>
|
||||
<image class="chat-avatar" :src="item.targetAvatar || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<view class="avatar-wrap">
|
||||
<image class="chat-avatar" :src="item.targetAvatar || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<view v-if="item.imUnread > 0" class="unread-badge">
|
||||
<text>{{ item.imUnread > 99 ? '99+' : item.imUnread }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chat-info">
|
||||
<view class="chat-top">
|
||||
<text class="chat-name">{{ displayNickname(item) }}</text>
|
||||
|
|
@ -75,6 +80,7 @@
|
|||
|
||||
<script>
|
||||
import { getUnreadCount, getChatOrderList } from '../../utils/api'
|
||||
import { getConversationList, initIM } from '../../utils/im'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -86,7 +92,8 @@ export default {
|
|||
totalUnread: 0
|
||||
},
|
||||
// 聊天记录列表(腾讯 IM SDK 集成后从 SDK 获取)
|
||||
chatList: []
|
||||
chatList: [],
|
||||
imUnreadTotal: 0
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
|
|
@ -112,11 +119,29 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
/** 加载聊天记录列表 */
|
||||
/** 加载聊天记录列表(含IM未读数) */
|
||||
async loadChatList() {
|
||||
try {
|
||||
const list = await getChatOrderList()
|
||||
this.chatList = list || []
|
||||
this.chatList = (list || []).map(item => ({ ...item, imUnread: 0 }))
|
||||
// 从 IM 获取未读数
|
||||
try {
|
||||
await initIM()
|
||||
const convList = await getConversationList()
|
||||
const unreadMap = {}
|
||||
convList.forEach(c => { unreadMap[c.targetUserId] = c.unreadCount || 0 })
|
||||
let totalImUnread = 0
|
||||
this.chatList.forEach(item => {
|
||||
const imUserId = `user_${item.targetUserId}`
|
||||
item.imUnread = unreadMap[imUserId] || 0
|
||||
totalImUnread += item.imUnread
|
||||
})
|
||||
// 更新 tabBar badge(系统未读 + IM未读)
|
||||
this.imUnreadTotal = totalImUnread
|
||||
this.updateTabBarBadge()
|
||||
} catch (e) {
|
||||
// IM 未登录时静默处理
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[消息页] 加载聊天列表失败:', e)
|
||||
}
|
||||
|
|
@ -124,7 +149,7 @@ export default {
|
|||
|
||||
/** 更新底部导航栏未读数 badge */
|
||||
updateTabBarBadge() {
|
||||
const total = this.unreadCount.totalUnread
|
||||
const total = (this.unreadCount.totalUnread || 0) + (this.imUnreadTotal || 0)
|
||||
if (total > 0) {
|
||||
uni.setTabBarBadge({
|
||||
index: 2,
|
||||
|
|
@ -316,10 +341,37 @@ export default {
|
|||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-wrap .chat-avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
background: #ff4d4f;
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
border-radius: 18rpx;
|
||||
padding: 0 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unread-badge text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chat-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -356,14 +356,13 @@ export default {
|
|||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 32rpx;
|
||||
font-size: 40rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.amount-value.highlight {
|
||||
font-size: 40rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<image class="user-avatar" :src="userInfo.avatarUrl || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ isLoggedIn ? (userInfo.nickname || '用户') : '点击注册/登录' }}</text>
|
||||
<text class="user-uid" v-if="isLoggedIn">UID:{{ userInfo.id }}</text>
|
||||
<text class="user-uid" v-if="isLoggedIn">UID:{{ userInfo.uid || userInfo.id }}</text>
|
||||
</view>
|
||||
<image class="arrow-icon" src="/static/ic_arrow.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -170,7 +170,8 @@
|
|||
acceptOrder,
|
||||
getCertificationStatus,
|
||||
submitCertification,
|
||||
getPageBanner
|
||||
getPageBanner,
|
||||
getServiceEntries
|
||||
} from '../../utils/api'
|
||||
|
||||
export default {
|
||||
|
|
@ -227,12 +228,29 @@
|
|||
onShow() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
// 重置认证状态缓存,确保每次进入页面重新获取
|
||||
this.certStatus = null
|
||||
this.loadBanner()
|
||||
this.loadTabs()
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
/** 从服务入口获取标签排序 */
|
||||
async loadTabs() {
|
||||
try {
|
||||
const entries = await getServiceEntries()
|
||||
if (entries && entries.length > 0) {
|
||||
// 服务入口名称到订单类型的映射
|
||||
const nameMap = { '代取': 'Pickup', '代送': 'Delivery', '万能帮': 'Help', '代购': 'Purchase', '美食街': 'Food' }
|
||||
const sorted = entries
|
||||
.filter(e => nameMap[e.name])
|
||||
.map(e => ({ label: e.name, value: nameMap[e.name] }))
|
||||
if (sorted.length > 0) {
|
||||
this.tabs = sorted
|
||||
this.currentTab = sorted[0].value
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
/** 加载顶部大图 */
|
||||
async loadBanner() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ export default {
|
|||
|
||||
getStatusLabel(status) {
|
||||
const map = {
|
||||
Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认',
|
||||
Unpaid: '待支付', Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认',
|
||||
Completed: '已完成', Cancelled: '已取消', Appealing: '申诉中'
|
||||
}
|
||||
return map[status] || status
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ export default {
|
|||
|
||||
getStatusLabel(status) {
|
||||
const map = {
|
||||
Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认',
|
||||
Unpaid: '待支付', Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认',
|
||||
Completed: '已完成', Cancelled: '已取消', Appealing: '申诉中'
|
||||
}
|
||||
return map[status] || status
|
||||
|
|
@ -350,8 +350,16 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
/** 跳转聊天页 */
|
||||
/** 跳转聊天页(如果上一页就是聊天页,直接返回) */
|
||||
goChat() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length >= 2) {
|
||||
const prevPage = pages[pages.length - 2]
|
||||
if (prevPage.route === 'pages/message/chat') {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/message/chat?orderId=${this.orderId}` })
|
||||
},
|
||||
|
||||
|
|
@ -745,6 +753,10 @@ export default {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { createOrder, getPageBanner, getMinCommission } from '../../utils/api'
|
||||
import { createOrder, getPageBanner, getMinCommission, cancelOrder, confirmPayment } from '../../utils/api'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -141,8 +141,8 @@ export default {
|
|||
uni.showToast({ title: `跑腿佣金不可低于${this.minCommission}元`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (val.includes('.') && val.split('.')[1].length > 1) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后1位', icon: 'none' })
|
||||
if (val.includes('.') && val.split('.')[1].length > 2) {
|
||||
uni.showToast({ title: '佣金最多支持小数点后2位', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -191,7 +191,17 @@ export default {
|
|||
commission,
|
||||
totalAmount: commission
|
||||
})
|
||||
if (result.paymentParams) await this.wxPay(result.paymentParams)
|
||||
if (result.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(result.paymentParams)
|
||||
// 支付成功,确认订单上架
|
||||
try { await confirmPayment(result.id) } catch (ex) {}
|
||||
} catch (e) {
|
||||
// 支付失败/取消,撤销订单
|
||||
try { await cancelOrder(result.id) } catch (ex) {}
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.showToast({ title: '下单成功', icon: 'success' })
|
||||
setTimeout(() => { uni.switchTab({ url: '/pages/index/index' }) }, 1500)
|
||||
} catch (e) {} finally { this.submitting = false }
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
import {
|
||||
createOrder,
|
||||
getPageBanner,
|
||||
getMinCommission
|
||||
getMinCommission,
|
||||
cancelOrder,
|
||||
confirmPayment
|
||||
} from '../../utils/api'
|
||||
|
||||
export default {
|
||||
|
|
@ -163,9 +165,9 @@
|
|||
});
|
||||
return false
|
||||
}
|
||||
if (val.includes('.') && val.split('.')[1].length > 1) {
|
||||
if (val.includes('.') && val.split('.')[1].length > 2) {
|
||||
uni.showToast({
|
||||
title: '佣金最多支持小数点后1位',
|
||||
title: '佣金最多支持小数点后2位',
|
||||
icon: 'none'
|
||||
});
|
||||
return false
|
||||
|
|
@ -252,7 +254,17 @@
|
|||
commission,
|
||||
totalAmount: goodsAmount + commission
|
||||
})
|
||||
if (result.paymentParams) await this.wxPay(result.paymentParams)
|
||||
if (result.paymentParams) {
|
||||
try {
|
||||
await this.wxPay(result.paymentParams)
|
||||
// 支付成功,确认订单上架
|
||||
try { await confirmPayment(result.id) } catch (ex) {}
|
||||
} catch (e) {
|
||||
// 支付失败/取消,撤销订单
|
||||
try { await cancelOrder(result.id) } catch (ex) {}
|
||||
return
|
||||
}
|
||||
}
|
||||
uni.showToast({
|
||||
title: '下单成功',
|
||||
icon: 'success'
|
||||
|
|
|
|||
|
|
@ -121,10 +121,6 @@ export const useCartStore = defineStore('cart', () => {
|
|||
if (shop.items[index].quantity <= 0) {
|
||||
shop.items.splice(index, 1)
|
||||
}
|
||||
// 如果门店购物车为空,移除门店
|
||||
if (shop.items.length === 0) {
|
||||
delete shops.value[currentShopId.value]
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空所有购物车 */
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ export function cancelOrder(id) {
|
|||
return request({ url: `/api/orders/${id}/cancel`, method: 'POST' })
|
||||
}
|
||||
|
||||
/** 确认支付成功(前端支付成功后调用) */
|
||||
export function confirmPayment(id) {
|
||||
return request({ url: `/api/orders/${id}/pay-confirm`, method: 'POST' })
|
||||
}
|
||||
|
||||
/** 跑腿提交完成 */
|
||||
export function completeOrder(id, data) {
|
||||
return request({ url: `/api/orders/${id}/complete`, method: 'POST', data })
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* 负责初始化、登录、收发消息
|
||||
*/
|
||||
import TencentCloudChat from '@tencentcloud/chat'
|
||||
import TIMUploadPlugin from 'tim-upload-plugin'
|
||||
import request from './request'
|
||||
|
||||
let chat = null
|
||||
|
|
@ -28,6 +29,8 @@ export async function initIM() {
|
|||
// 创建 SDK 实例
|
||||
if (!chat) {
|
||||
chat = TencentCloudChat.create({ SDKAppID: sdkAppId })
|
||||
// 注册上传插件(发送图片/文件必需)
|
||||
chat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin })
|
||||
chat.setLogLevel(1) // Release 级别日志
|
||||
|
||||
// 监听 SDK 就绪
|
||||
|
|
@ -189,14 +192,14 @@ export async function sendTextMessage(targetUserId, text, orderId) {
|
|||
/**
|
||||
* 发送图片消息
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
* @param {string} filePath - 本地图片路径
|
||||
* @param {Object} fileRes - uni.chooseImage 的 success 回调结果
|
||||
* @param {string|number} [orderId] - 关联订单ID
|
||||
*/
|
||||
export async function sendImageMessage(targetUserId, filePath, orderId) {
|
||||
export async function sendImageMessage(targetUserId, fileRes, orderId) {
|
||||
const message = chat.createImageMessage({
|
||||
to: targetUserId,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
payload: { file: { tempFilePaths: [filePath] } }
|
||||
payload: { file: fileRes }
|
||||
})
|
||||
if (orderId) message.cloudCustomData = JSON.stringify({ orderId: String(orderId) })
|
||||
const res = await chat.sendMessage(message)
|
||||
|
|
@ -289,6 +292,20 @@ export function formatIMMessage(msg, currentImUserId) {
|
|||
orderId: msgOrderId
|
||||
}
|
||||
}
|
||||
if (data.bizType === 'price-change-response') {
|
||||
return {
|
||||
id: msg.ID,
|
||||
type: 'price-change-response',
|
||||
priceChangeId: data.priceChangeId,
|
||||
action: data.action,
|
||||
status: data.status,
|
||||
changeTypeLabel: data.changeTypeLabel,
|
||||
content: data.description || '',
|
||||
isSelf,
|
||||
time: msg.time * 1000,
|
||||
orderId: msgOrderId
|
||||
}
|
||||
}
|
||||
if (data.bizType === 'order-status') {
|
||||
return {
|
||||
id: msg.ID,
|
||||
|
|
|
|||
|
|
@ -227,7 +227,8 @@ public static class AdminEndpoints
|
|||
return new
|
||||
{
|
||||
r.Id, r.Nickname, r.Phone, r.RunnerScore, r.IsBanned, r.CreatedAt,
|
||||
AvgRating = stats?.AvgRating ?? 0,
|
||||
// 评价星级:基于评分计算,每20分一颗星,最低1星最高5星
|
||||
StarRating = Math.Round(Math.Clamp(r.RunnerScore / 20.0, 1.0, 5.0), 1),
|
||||
ReviewCount = stats?.ReviewCount ?? 0
|
||||
};
|
||||
}).ToList();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using CampusErrand.Data;
|
|||
using CampusErrand.Models;
|
||||
using CampusErrand.Models.Dtos;
|
||||
using CampusErrand.Services;
|
||||
using CampusErrand.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CampusErrand.Endpoints;
|
||||
|
|
@ -42,6 +43,7 @@ public static class AuthEndpoints
|
|||
{
|
||||
user = new User
|
||||
{
|
||||
Uid = await BusinessHelpers.GenerateUniqueUid(db),
|
||||
OpenId = sessionResult.OpenId!,
|
||||
Phone = phoneNumber,
|
||||
Nickname = $"用户{phoneNumber[^4..]}",
|
||||
|
|
@ -66,6 +68,7 @@ public static class AuthEndpoints
|
|||
UserInfo = new UserInfo
|
||||
{
|
||||
Id = user.Id,
|
||||
Uid = user.Uid,
|
||||
Phone = user.Phone,
|
||||
Nickname = user.Nickname,
|
||||
AvatarUrl = user.AvatarUrl,
|
||||
|
|
@ -98,6 +101,7 @@ public static class AuthEndpoints
|
|||
var randomSuffix = Random.Shared.Next(1000, 9999).ToString();
|
||||
user = new User
|
||||
{
|
||||
Uid = await BusinessHelpers.GenerateUniqueUid(db),
|
||||
OpenId = openId,
|
||||
Phone = "",
|
||||
Nickname = $"微信用户{randomSuffix}",
|
||||
|
|
@ -116,6 +120,7 @@ public static class AuthEndpoints
|
|||
UserInfo = new UserInfo
|
||||
{
|
||||
Id = user.Id,
|
||||
Uid = user.Uid,
|
||||
Phone = user.Phone,
|
||||
Nickname = user.Nickname,
|
||||
AvatarUrl = user.AvatarUrl,
|
||||
|
|
|
|||
|
|
@ -179,6 +179,26 @@ public static class MessageEndpoints
|
|||
return Results.Ok(orders);
|
||||
}).RequireAuthorization();
|
||||
|
||||
// 管理端获取已发送通知列表
|
||||
app.MapGet("/api/admin/notifications", async (AppDbContext db) =>
|
||||
{
|
||||
var list = await db.SystemMessages
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.Select(m => new
|
||||
{
|
||||
m.Id,
|
||||
m.Title,
|
||||
ContentPreview = m.Content.Length > 80 ? m.Content.Substring(0, 80) + "…" : m.Content,
|
||||
m.ThumbnailUrl,
|
||||
TargetType = m.TargetType.ToString(),
|
||||
m.TargetUserIds,
|
||||
m.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(list);
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端发布系统通知
|
||||
app.MapPost("/api/admin/notifications", async (CreateNotificationRequest request, AppDbContext db) =>
|
||||
{
|
||||
|
|
@ -218,5 +238,21 @@ public static class MessageEndpoints
|
|||
CreatedAt = message.CreatedAt
|
||||
});
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端删除通知
|
||||
app.MapDelete("/api/admin/notifications/{id}", async (int id, AppDbContext db) =>
|
||||
{
|
||||
var msg = await db.SystemMessages.FindAsync(id);
|
||||
if (msg == null)
|
||||
return Results.NotFound(new { code = 404, message = "通知不存在" });
|
||||
|
||||
// 删除关联的已读记录
|
||||
db.MessageReads.RemoveRange(
|
||||
db.MessageReads.Where(r => r.MessageType == MessageType.System && r.MessageId == id));
|
||||
db.SystemMessages.Remove(msg);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { message = "已删除" });
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ public static class OrderEndpoints
|
|||
return Results.BadRequest(new { code = 400, message = $"跑腿佣金不可低于{minCommission}元" });
|
||||
}
|
||||
|
||||
// 佣金校验:小数点后最多 1 位
|
||||
if (request.Commission != Math.Round(request.Commission, 1))
|
||||
// 佣金校验:小数点后最多 2 位
|
||||
if (request.Commission != Math.Round(request.Commission, 2))
|
||||
{
|
||||
return Results.BadRequest(new { code = 400, message = "跑腿佣金最多支持小数点后1位" });
|
||||
return Results.BadRequest(new { code = 400, message = "跑腿佣金最多支持小数点后2位" });
|
||||
}
|
||||
|
||||
// 计算支付总金额
|
||||
|
|
@ -89,7 +89,7 @@ public static class OrderEndpoints
|
|||
OrderNo = orderNo,
|
||||
OwnerId = userId,
|
||||
OrderType = orderType,
|
||||
Status = OrderStatus.Pending,
|
||||
Status = OrderStatus.Unpaid,
|
||||
ItemName = request.ItemName,
|
||||
PickupLocation = request.PickupLocation,
|
||||
DeliveryLocation = request.DeliveryLocation,
|
||||
|
|
@ -547,19 +547,41 @@ public static class OrderEndpoints
|
|||
|
||||
if (result.TradeState == "SUCCESS")
|
||||
{
|
||||
// 支付成功,更新订单状态(如果还是 Pending 说明支付完成)
|
||||
// 支付成功,将待支付订单改为待接单
|
||||
var order = await db.Orders.FirstOrDefaultAsync(o => o.OrderNo == result.OrderNo);
|
||||
if (order != null)
|
||||
if (order != null && order.Status == OrderStatus.Unpaid)
|
||||
{
|
||||
Console.WriteLine($"[微信支付] 支付成功: {result.OrderNo}, 金额: {result.TotalAmount}分");
|
||||
order.Status = OrderStatus.Pending;
|
||||
await db.SaveChangesAsync();
|
||||
Console.WriteLine($"[微信支付] 支付成功,订单已上架: {result.OrderNo}, 金额: {result.TotalAmount}分");
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Json(new { code = "SUCCESS", message = "OK" });
|
||||
}).AllowAnonymous();
|
||||
|
||||
// 取消订单(仅单主可取消待接单订单)
|
||||
app.MapPost("/api/orders/{id}/cancel", async (int id, HttpContext httpContext, AppDbContext db) =>
|
||||
// 前端支付成功后确认订单(本地开发用,生产环境由回调处理)
|
||||
app.MapPost("/api/orders/{id}/pay-confirm", async (int id, 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id && o.OwnerId == userId);
|
||||
if (order == null)
|
||||
return Results.NotFound(new { code = 404, message = "订单不存在" });
|
||||
|
||||
if (order.Status == OrderStatus.Unpaid)
|
||||
{
|
||||
order.Status = OrderStatus.Pending;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Results.Ok(new { id = order.Id, status = order.Status.ToString() });
|
||||
}).RequireAuthorization();
|
||||
|
||||
// 取消订单(仅单主可取消待支付或待接单订单,自动退款)
|
||||
app.MapPost("/api/orders/{id}/cancel", async (int id, HttpContext httpContext, AppDbContext db, WxPayService wxPay) =>
|
||||
{
|
||||
var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
if (userIdClaim == null) return Results.Unauthorized();
|
||||
|
|
@ -572,13 +594,31 @@ public static class OrderEndpoints
|
|||
if (order.OwnerId != userId)
|
||||
return Results.BadRequest(new { code = 400, message = "仅单主可取消订单" });
|
||||
|
||||
if (order.Status != OrderStatus.Pending)
|
||||
return Results.BadRequest(new { code = 400, message = "仅待接单状态的订单可取消" });
|
||||
if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Unpaid)
|
||||
return Results.BadRequest(new { code = 400, message = "仅待支付或待接单状态的订单可取消" });
|
||||
|
||||
var wasPaid = order.Status == OrderStatus.Pending; // 已支付的才需要退款
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString() });
|
||||
// 已支付的订单发起微信退款(原路返回)
|
||||
var refundSuccess = false;
|
||||
if (wasPaid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var totalFen = (int)(order.TotalAmount * 100);
|
||||
var refundNo = $"R{order.OrderNo}";
|
||||
refundSuccess = await wxPay.Refund(order.OrderNo, refundNo, totalFen, totalFen, "用户取消订单");
|
||||
Console.WriteLine($"[订单取消] {order.OrderNo} 退款{(refundSuccess ? "成功" : "失败")}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[订单取消] {order.OrderNo} 退款异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString(), refundSuccess });
|
||||
}).RequireAuthorization();
|
||||
|
||||
// 跑腿提交完成
|
||||
|
|
@ -915,7 +955,7 @@ public static class OrderEndpoints
|
|||
}).RequireAuthorization();
|
||||
|
||||
// 响应改价(同意或拒绝)
|
||||
app.MapPut("/api/orders/{id}/price-change/{changeId}", async (int id, int changeId, RespondPriceChangeRequest request, HttpContext httpContext, AppDbContext db) =>
|
||||
app.MapPut("/api/orders/{id}/price-change/{changeId}", async (int id, int changeId, RespondPriceChangeRequest request, HttpContext httpContext, AppDbContext db, WxPayService wxPay) =>
|
||||
{
|
||||
var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
if (userIdClaim == null) return Results.Unauthorized();
|
||||
|
|
@ -931,11 +971,9 @@ public static class OrderEndpoints
|
|||
if (priceChange.Status != PriceChangeStatus.Pending)
|
||||
return Results.BadRequest(new { code = 400, message = "该改价申请已处理" });
|
||||
|
||||
// 不能自己响应自己的改价申请
|
||||
if (priceChange.InitiatorId == userId)
|
||||
return Results.BadRequest(new { code = 400, message = "不能响应自己的改价申请" });
|
||||
|
||||
// 仅订单相关方可响应
|
||||
var order = priceChange.Order!;
|
||||
if (userId != order.OwnerId && userId != order.RunnerId)
|
||||
return Results.BadRequest(new { code = 400, message = "仅订单相关方可响应改价" });
|
||||
|
|
@ -945,35 +983,78 @@ public static class OrderEndpoints
|
|||
return Results.BadRequest(new { code = 400, message = "操作不合法,仅支持 Accepted 或 Rejected" });
|
||||
|
||||
priceChange.Status = action;
|
||||
var difference = priceChange.NewPrice - priceChange.OriginalPrice;
|
||||
object? paymentParams = null;
|
||||
var refundSuccess = false;
|
||||
|
||||
// 同意改价时更新订单金额
|
||||
if (action == PriceChangeStatus.Accepted)
|
||||
{
|
||||
var oldTotal = order.TotalAmount;
|
||||
|
||||
if (priceChange.ChangeType == PriceChangeType.Commission)
|
||||
{
|
||||
order.Commission = priceChange.NewPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
order.GoodsAmount = priceChange.NewPrice;
|
||||
}
|
||||
|
||||
// 重新计算支付总金额
|
||||
if (order.OrderType == OrderType.Help || order.OrderType == OrderType.Purchase || order.OrderType == OrderType.Food)
|
||||
{
|
||||
order.TotalAmount = (order.GoodsAmount ?? 0) + order.Commission;
|
||||
}
|
||||
else
|
||||
{
|
||||
order.TotalAmount = order.Commission;
|
||||
|
||||
var totalDiff = order.TotalAmount - oldTotal;
|
||||
|
||||
// 需要补价:创建微信支付订单
|
||||
if (totalDiff > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var owner = await db.Users.FindAsync(order.OwnerId);
|
||||
if (owner != null && !string.IsNullOrEmpty(owner.OpenId))
|
||||
{
|
||||
var notifyUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/pay/notify";
|
||||
var payOrderNo = $"P{order.OrderNo}{DateTime.UtcNow:mmss}";
|
||||
var payResult = await wxPay.CreateJsapiOrder(payOrderNo, totalDiff, "校园跑腿-改价补缴", owner.OpenId, notifyUrl);
|
||||
if (payResult.Success)
|
||||
{
|
||||
paymentParams = new
|
||||
{
|
||||
timeStamp = payResult.PaymentParams!.TimeStamp,
|
||||
nonceStr = payResult.PaymentParams.NonceStr,
|
||||
package_ = payResult.PaymentParams.Package,
|
||||
signType = payResult.PaymentParams.SignType,
|
||||
paySign = payResult.PaymentParams.PaySign
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[改价] 补价支付创建失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
// 需要退款:自动退款
|
||||
else if (totalDiff < 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var refundAmount = Math.Abs(totalDiff);
|
||||
var refundFen = (int)(refundAmount * 100);
|
||||
var totalFen = (int)(oldTotal * 100);
|
||||
var refundNo = $"RP{order.OrderNo}{DateTime.UtcNow:mmss}";
|
||||
refundSuccess = await wxPay.Refund(order.OrderNo, refundNo, totalFen, refundFen, "改价退款");
|
||||
Console.WriteLine($"[改价] 退款{(refundSuccess ? "成功" : "失败")}: ¥{refundAmount}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[改价] 退款失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var difference = priceChange.NewPrice - priceChange.OriginalPrice;
|
||||
|
||||
return Results.Ok(new PriceChangeResponse
|
||||
return Results.Ok(new
|
||||
{
|
||||
Id = priceChange.Id,
|
||||
OrderId = priceChange.OrderId,
|
||||
|
|
@ -983,7 +1064,9 @@ public static class OrderEndpoints
|
|||
NewPrice = priceChange.NewPrice,
|
||||
Difference = difference,
|
||||
Status = priceChange.Status.ToString(),
|
||||
CreatedAt = priceChange.CreatedAt
|
||||
CreatedAt = priceChange.CreatedAt,
|
||||
PaymentParams = paymentParams,
|
||||
RefundSuccess = refundSuccess
|
||||
});
|
||||
}).RequireAuthorization();
|
||||
|
||||
|
|
|
|||
|
|
@ -193,13 +193,29 @@ public static class ShopEndpoints
|
|||
return Results.NotFound(new { code = 404, message = "门店不存在" });
|
||||
}
|
||||
|
||||
// 级联删除菜品和门店 Banner
|
||||
db.Dishes.RemoveRange(shop.Dishes);
|
||||
db.ShopBanners.RemoveRange(shop.ShopBanners);
|
||||
db.Shops.Remove(shop);
|
||||
await db.SaveChangesAsync();
|
||||
try
|
||||
{
|
||||
// 先删除订单中引用该门店的菜品项
|
||||
var dishIds = shop.Dishes.Select(d => d.Id).ToList();
|
||||
db.FoodOrderItems.RemoveRange(db.FoodOrderItems.Where(f => f.ShopId == id || dishIds.Contains(f.DishId)));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.NoContent();
|
||||
// 再删除菜品和门店 Banner
|
||||
db.Dishes.RemoveRange(shop.Dishes);
|
||||
db.ShopBanners.RemoveRange(shop.ShopBanners);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// 最后删除门店
|
||||
db.Shops.Remove(shop);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[管理端] 删除门店失败: {ex.InnerException?.Message ?? ex.Message}");
|
||||
return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.InnerException?.Message ?? ex.Message}" });
|
||||
}
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 获取门店 Banner 列表
|
||||
|
|
@ -351,10 +367,22 @@ public static class ShopEndpoints
|
|||
return Results.NotFound(new { code = 404, message = "菜品不存在" });
|
||||
}
|
||||
|
||||
db.Dishes.Remove(dish);
|
||||
await db.SaveChangesAsync();
|
||||
try
|
||||
{
|
||||
// 先删除订单中引用该菜品的记录
|
||||
db.FoodOrderItems.RemoveRange(db.FoodOrderItems.Where(f => f.DishId == dishId));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.NoContent();
|
||||
db.Dishes.Remove(dish);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[管理端] 删除菜品失败: {ex.InnerException?.Message ?? ex.Message}");
|
||||
return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.InnerException?.Message ?? ex.Message}" });
|
||||
}
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,4 +188,19 @@ public static class BusinessHelpers
|
|||
Console.WriteLine($"[定时任务] 自动确认了 {expiredOrders.Count} 个超时订单");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成唯一的随机6位数字UID
|
||||
/// </summary>
|
||||
internal static async Task<string> GenerateUniqueUid(AppDbContext db)
|
||||
{
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var uid = Random.Shared.Next(100000, 999999).ToString();
|
||||
var exists = await db.Users.AnyAsync(u => u.Uid == uid);
|
||||
if (!exists) return uid;
|
||||
}
|
||||
// 极端情况:用7位
|
||||
return Random.Shared.Next(1000000, 9999999).ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
912
server/Migrations/20260401032529_AddUserUid.Designer.cs
generated
Normal file
912
server/Migrations/20260401032529_AddUserUid.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,912 @@
|
|||
// <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("20260401032529_AddUserUid")]
|
||||
partial class AddUserUid
|
||||
{
|
||||
/// <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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
30
server/Migrations/20260401032529_AddUserUid.cs
Normal file
30
server/Migrations/20260401032529_AddUserUid.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CampusErrand.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserUid : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Uid",
|
||||
table: "Users",
|
||||
type: "nvarchar(10)",
|
||||
maxLength: 10,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Uid",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -669,6 +669,11 @@ namespace CampusErrand.Migrations
|
|||
b.Property<int>("RunnerScore")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OpenId")
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public class LoginResponse
|
|||
public class UserInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Uid { get; set; } = string.Empty;
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
public string AvatarUrl { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public enum OrderType
|
|||
/// </summary>
|
||||
public enum OrderStatus
|
||||
{
|
||||
Unpaid = -1, // 待支付
|
||||
Pending = 0, // 待接单
|
||||
InProgress = 1, // 进行中
|
||||
WaitConfirm = 2, // 待确认
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ public class User
|
|||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>展示用 UID(随机6位数字)</summary>
|
||||
[MaxLength(10)]
|
||||
public string Uid { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>微信 OpenID</summary>
|
||||
[MaxLength(128)]
|
||||
public string OpenId { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -73,12 +73,13 @@ public class TencentIMService
|
|||
Peer_Account = toUserId,
|
||||
MaxCnt = maxCnt,
|
||||
MinTime = minTime,
|
||||
MaxTime = maxTime
|
||||
MaxTime = maxTime > 0 ? maxTime : DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
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] 拉取漫游消息: {fromUserId} -> {toUserId}, 响应: {json}");
|
||||
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user