细节优化
This commit is contained in:
parent
365116c2f8
commit
d341e859dc
|
|
@ -50,6 +50,10 @@
|
|||
<el-icon><List /></el-icon>
|
||||
<template #title>订单管理</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/withdrawals">
|
||||
<el-icon><Money /></el-icon>
|
||||
<template #title>提现管理</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/config">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<template #title>配置管理</template>
|
||||
|
|
@ -91,7 +95,7 @@
|
|||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { Monitor, Fold, Expand, ArrowDown, Picture, Grid, Shop, Stamp, User, Star, Bell, List, Setting } from '@element-plus/icons-vue'
|
||||
import { Monitor, Fold, Expand, ArrowDown, Picture, Grid, Shop, Stamp, User, Star, Bell, List, Setting, Money } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const isCollapse = ref(false)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ const routes = [
|
|||
component: () => import('../views/Orders.vue'),
|
||||
meta: { title: '订单管理' }
|
||||
},
|
||||
{
|
||||
path: 'withdrawals',
|
||||
name: 'Withdrawals',
|
||||
component: () => import('../views/Withdrawals.vue'),
|
||||
meta: { title: '提现管理' }
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'Config',
|
||||
|
|
|
|||
|
|
@ -84,6 +84,18 @@
|
|||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 跑腿协议 -->
|
||||
<el-tab-pane label="跑腿协议" name="runner_agreement">
|
||||
<el-form label-width="100px" style="max-width: 700px;">
|
||||
<el-form-item label="协议内容">
|
||||
<el-input v-model="configs.runner_agreement" type="textarea" :rows="12" placeholder="请输入跑腿协议内容" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveConfig('runner_agreement')">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 提现说明 -->
|
||||
<el-tab-pane label="提现说明" name="withdrawal_guide">
|
||||
<el-form label-width="100px" style="max-width: 700px;">
|
||||
|
|
@ -154,7 +166,7 @@ const uploadHeaders = { Authorization: `Bearer ${localStorage.getItem('admin_tok
|
|||
const commissionRules = ref([])
|
||||
|
||||
// 系统配置
|
||||
const configs = reactive({ qrcode: '', agreement: '', privacy: '', withdrawal_guide: '' })
|
||||
const configs = reactive({ qrcode: '', agreement: '', privacy: '', runner_agreement: '', withdrawal_guide: '' })
|
||||
const freezeDays = ref(1)
|
||||
|
||||
// 页面顶图配置
|
||||
|
|
@ -256,6 +268,7 @@ onMounted(async () => {
|
|||
fetchConfig('qrcode'),
|
||||
fetchConfig('agreement'),
|
||||
fetchConfig('privacy'),
|
||||
fetchConfig('runner_agreement'),
|
||||
fetchConfig('withdrawal_guide'),
|
||||
fetchFreezeDays(),
|
||||
fetchPageBanners()
|
||||
|
|
|
|||
|
|
@ -1,21 +1,61 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3 style="margin: 0 0 16px;">跑腿管理</h3>
|
||||
<div class="runners-page">
|
||||
<div class="page-header">
|
||||
<h2>跑腿管理</h2>
|
||||
<el-tag type="info" size="large">共 {{ list.length }} 名跑腿</el-tag>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="nickname" label="昵称" width="120" />
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
<el-table-column prop="runnerScore" label="评分" width="80" />
|
||||
<el-table-column label="封禁状态" width="100">
|
||||
<el-table :data="list" 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="140" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" min-width="140" />
|
||||
<el-table-column label="评分" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isBanned ? 'danger' : 'success'">{{ row.isBanned ? '已封禁' : '正常' }}</el-tag>
|
||||
<div class="score-cell">
|
||||
<el-progress
|
||||
:percentage="row.runnerScore"
|
||||
:color="getScoreColor(row.runnerScore)"
|
||||
:stroke-width="10"
|
||||
:show-text="false"
|
||||
style="width: 80px; display: inline-block; vertical-align: middle;"
|
||||
/>
|
||||
<span class="score-text" :style="{ color: getScoreColor(row.runnerScore) }">{{ row.runnerScore }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="!row.isBanned" size="small" type="danger" @click="toggleBan(row, true)">封禁</el-button>
|
||||
<el-button v-else size="small" type="success" @click="toggleBan(row, false)">解封</el-button>
|
||||
<el-tag :type="row.isBanned ? 'danger' : 'success'" round size="small">
|
||||
{{ row.isBanned ? '已封禁' : '正常' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="注册时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm
|
||||
v-if="!row.isBanned"
|
||||
title="确定封禁该跑腿?"
|
||||
confirm-button-text="封禁"
|
||||
confirm-button-type="danger"
|
||||
@confirm="toggleBan(row, true)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger" plain>封禁</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-popconfirm
|
||||
v-else
|
||||
title="确定解封该跑腿?"
|
||||
confirm-button-text="解封"
|
||||
@confirm="toggleBan(row, false)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" type="success" plain>解封</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -24,7 +64,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
|
|
@ -41,11 +81,47 @@ async function fetchList() {
|
|||
|
||||
async function toggleBan(row, isBanned) {
|
||||
const label = isBanned ? '封禁' : '解封'
|
||||
await ElMessageBox.confirm(`确定${label}跑腿「${row.nickname}」?`, '提示', { type: 'warning' })
|
||||
await request.put(`/admin/runners/${row.id}/ban`, { isBanned })
|
||||
ElMessage.success(`已${label}`)
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function getScoreColor(score) {
|
||||
if (score >= 80) return '#67c23a'
|
||||
if (score >= 60) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
function formatTime(str) {
|
||||
if (!str) return '-'
|
||||
const d = new Date(str)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
.score-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.score-text {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
min-width: 24px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
126
admin/src/views/Withdrawals.vue
Normal file
126
admin/src/views/Withdrawals.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="withdrawals-page">
|
||||
<div class="page-header">
|
||||
<h2>提现管理</h2>
|
||||
<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="amount" label="金额" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #e64340; font-weight: bold">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="paymentMethod" label="收款方式" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.paymentMethod === 'WeChat' ? 'success' : 'primary'" round size="small">
|
||||
{{ row.paymentMethod === 'WeChat' ? '微信' : '支付宝' }}
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="申请时间" min-width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</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'">
|
||||
<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>
|
||||
<span style="color: #999">已处理</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const statusFilter = ref('')
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = statusFilter.value ? `?status=${statusFilter.value}` : ''
|
||||
const res = await request.get(`/admin/withdrawals${params}`)
|
||||
list.value = res || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(row, action) {
|
||||
const labels = { approve: '通过', reject: '拒绝', processing: '标记为处理中' }
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定${labels[action]}该提现申请?`, '提示', { type: 'warning' })
|
||||
await request.put(`/admin/withdrawals/${row.id}`, { action })
|
||||
ElMessage.success('操作成功')
|
||||
loadData()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error(e?.response?.data?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(s) {
|
||||
return { Pending: '待处理', Processing: '处理中', Completed: '已完成' }[s] || s
|
||||
}
|
||||
|
||||
function statusTagType(s) {
|
||||
return { Pending: 'warning', Processing: '', Completed: 'success' }[s] || 'info'
|
||||
}
|
||||
|
||||
function formatTime(str) {
|
||||
if (!str) return '-'
|
||||
const d = new Date(str)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -181,6 +181,13 @@
|
|||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/config/runner-agreement",
|
||||
"style": {
|
||||
"navigationBarTitleText": "跑腿协议",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/webview/webview",
|
||||
"style": {
|
||||
|
|
@ -197,7 +204,7 @@
|
|||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#007AFF",
|
||||
"selectedColor": "#FFB700",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"list": [
|
||||
|
|
|
|||
105
miniapp/pages/config/runner-agreement.vue
Normal file
105
miniapp/pages/config/runner-agreement.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<view class="agreement-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="navbar-title">跑腿协议</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<rich-text v-if="content" :nodes="content" class="rich-content"></rich-text>
|
||||
<view v-else class="empty-tip">
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRunnerAgreement } from '../../utils/api'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
statusBarHeight: 0
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
this.loadContent()
|
||||
},
|
||||
methods: {
|
||||
goBack() { uni.navigateBack() },
|
||||
/** 加载跑腿协议内容 */
|
||||
async loadContent() {
|
||||
try {
|
||||
const res = await getRunnerAgreement()
|
||||
this.content = res?.value || res?.content || ''
|
||||
} catch (e) {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
background: #FFB700;
|
||||
}
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.back-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
.navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
.agreement-page {
|
||||
min-height: 100vh;
|
||||
background-color: #ffffff;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.rich-content {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
.empty-tip text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -12,11 +12,21 @@
|
|||
</view>
|
||||
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<!-- 顶部订单信息栏 -->
|
||||
<view class="order-bar" v-if="orderInfo.id" @click="goOrderDetail">
|
||||
<text class="order-bar-text">
|
||||
{{ formatOrderType(orderInfo.orderType) }}订单 #{{ orderInfo.orderNo }}
|
||||
</text>
|
||||
<text class="order-bar-link">查看详情 ›</text>
|
||||
<view class="order-bar" v-if="orderInfo.id">
|
||||
<view class="order-bar-info" @click="goOrderDetail">
|
||||
<text class="order-bar-text">
|
||||
{{ formatOrderType(orderInfo.orderType) }}订单 #{{ orderInfo.orderNo }}
|
||||
</text>
|
||||
<text class="order-bar-link">查看详情 ›</text>
|
||||
</view>
|
||||
<view class="order-bar-actions">
|
||||
<view class="bar-action-btn" @click="onCallPhone">
|
||||
<text>📞 拨打电话</text>
|
||||
</view>
|
||||
<view class="bar-action-btn" @click="onContactService">
|
||||
<text>💬 联系客服</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天记录区域 -->
|
||||
|
|
@ -253,8 +263,12 @@ export default {
|
|||
async onLoad(options) {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||||
this.orderId = options.orderId
|
||||
await this.loadOrderInfo()
|
||||
this.orderId = options.orderId || null
|
||||
// 从消息列表跳转时可能只有 targetUserId
|
||||
this.targetImUserIdFromParam = options.targetUserId || null
|
||||
if (this.orderId) {
|
||||
await this.loadOrderInfo()
|
||||
}
|
||||
await this.loginIM()
|
||||
},
|
||||
onUnload() {
|
||||
|
|
@ -280,13 +294,18 @@ export default {
|
|||
this.imUserId = userId
|
||||
|
||||
// 确定对方 IM 用户 ID
|
||||
const userStore = useUserStore()
|
||||
if (this.orderInfo.ownerId === userStore.userId) {
|
||||
// 当前用户是单主,对方是跑腿
|
||||
this.targetImUserId = `user_${this.orderInfo.runnerId}`
|
||||
} else {
|
||||
// 当前用户是跑腿,对方是单主
|
||||
this.targetImUserId = `user_${this.orderInfo.ownerId}`
|
||||
if (this.targetImUserIdFromParam) {
|
||||
// 从消息列表跳转,直接使用传入的 targetUserId
|
||||
this.targetImUserId = this.targetImUserIdFromParam
|
||||
} else if (this.orderInfo.id) {
|
||||
const userStore = useUserStore()
|
||||
if (this.orderInfo.ownerId === userStore.userId) {
|
||||
// 当前用户是单主,对方是跑腿
|
||||
this.targetImUserId = `user_${this.orderInfo.runnerId}`
|
||||
} else {
|
||||
// 当前用户是跑腿,对方是单主
|
||||
this.targetImUserId = `user_${this.orderInfo.ownerId}`
|
||||
}
|
||||
}
|
||||
|
||||
this.imReady = true
|
||||
|
|
@ -519,6 +538,39 @@ export default {
|
|||
uni.navigateTo({ url: `/pages/order/order-detail?id=${this.orderId}` })
|
||||
},
|
||||
|
||||
/** 拨打电话(显示对方手机号) */
|
||||
onCallPhone() {
|
||||
// 单主看跑腿手机号,跑腿看单主手机号
|
||||
const phone = this.isOwner ? this.orderInfo.runnerPhone : this.orderInfo.phone
|
||||
if (!phone) {
|
||||
uni.showToast({ title: '暂无对方手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '对方手机号',
|
||||
content: phone,
|
||||
confirmText: '复制电话',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.setClipboardData({
|
||||
data: phone,
|
||||
success: () => {
|
||||
uni.showToast({ title: '手机号已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 联系客服 */
|
||||
onContactService() {
|
||||
// 跳转微信小程序自带客服页(需在小程序后台配置客服)
|
||||
// 小程序中使用 button open-type="contact" 才能打开客服
|
||||
// 这里用 navigateTo 跳转客服二维码页作为替代
|
||||
uni.navigateTo({ url: '/pages/config/qrcode' })
|
||||
},
|
||||
|
||||
previewImage(url) {
|
||||
uni.previewImage({ urls: [url] })
|
||||
},
|
||||
|
|
@ -573,12 +625,14 @@ export default {
|
|||
height: 100vh;
|
||||
}
|
||||
.order-bar {
|
||||
background-color: #f0f7ff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.order-bar-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #f0f7ff;
|
||||
padding: 20rpx 30rpx;
|
||||
flex-shrink: 0;
|
||||
padding: 20rpx 30rpx 10rpx;
|
||||
}
|
||||
.order-bar-text {
|
||||
font-size: 26rpx;
|
||||
|
|
@ -588,6 +642,23 @@ export default {
|
|||
font-size: 26rpx;
|
||||
color: #FFB700;
|
||||
}
|
||||
.order-bar-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 10rpx 30rpx 20rpx;
|
||||
}
|
||||
.bar-action-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12rpx 0;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
}
|
||||
.bar-action-btn text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
padding: 20rpx 24rpx;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
|
||||
<script>
|
||||
import { getUnreadCount } from '../../utils/api'
|
||||
import { getConversationList, getChatInstance } from '../../utils/im'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -109,10 +110,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
/** 加载聊天记录列表(腾讯 IM SDK 集成后替换) */
|
||||
loadChatList() {
|
||||
// TODO: 集成腾讯 IM SDK 后,从 SDK 获取会话列表
|
||||
// 当前为占位实现
|
||||
/** 加载聊天记录列表(从腾讯 IM SDK 获取会话列表) */
|
||||
async loadChatList() {
|
||||
try {
|
||||
const list = await getConversationList()
|
||||
this.chatList = list
|
||||
} catch (e) {
|
||||
console.error('[消息页] 加载聊天列表失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/** 更新底部导航栏未读数 badge */
|
||||
|
|
@ -140,8 +145,9 @@ export default {
|
|||
|
||||
/** 跳转聊天页 */
|
||||
goChat(item) {
|
||||
// IM 会话列表中没有 orderId,传 targetUserId 让聊天页自行查找关联订单
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/chat?orderId=${item.orderId}&targetUserId=${item.targetUserId}`
|
||||
url: `/pages/message/chat?targetUserId=${item.targetUserId}`
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
</view>
|
||||
<view class="notify-body">
|
||||
<text class="notify-order-no">订单编号:{{ item.orderNo }}</text>
|
||||
<text class="notify-item-name" v-if="item.itemName">{{ item.itemName }}</text>
|
||||
<text class="notify-item-name" v-if="item.itemName">
|
||||
{{ getFirstFieldLabel(item.orderType) }}:{{ item.itemName }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="notify-footer">
|
||||
<text class="notify-time">{{ formatTime(item.createdAt) }}</text>
|
||||
|
|
@ -120,6 +122,18 @@ export default {
|
|||
return map[type] || type
|
||||
},
|
||||
|
||||
/** 获取订单第1项字段标题 */
|
||||
getFirstFieldLabel(type) {
|
||||
const map = {
|
||||
Pickup: '代取物品',
|
||||
Delivery: '代送物品',
|
||||
Help: '帮忙事项',
|
||||
Purchase: '代购物品',
|
||||
Food: '美食订单'
|
||||
}
|
||||
return map[type] || '物品'
|
||||
},
|
||||
|
||||
/** 格式化时间(精确到年月日时分,) */
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
<text class="record-label">订单号</text>
|
||||
<text class="record-value">{{ item.orderNo }}</text>
|
||||
</view>
|
||||
<view class="record-row" v-if="item.completedAt">
|
||||
<text class="record-label">完成时间</text>
|
||||
<text class="record-value">{{ formatTime(item.completedAt) }}</text>
|
||||
</view>
|
||||
<view class="record-row" v-if="item.goodsAmount">
|
||||
<text class="record-label">垫付商品金额</text>
|
||||
<text class="record-value">¥{{ item.goodsAmount }}</text>
|
||||
|
|
@ -44,6 +48,11 @@
|
|||
<text class="record-value highlight">¥{{ item.netEarning }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-footer">
|
||||
<view class="view-order-btn" @click="goOrderDetail(item.orderId)">
|
||||
<text>查看订单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
|
@ -84,6 +93,12 @@ export default {
|
|||
return map[type] || type || '跑腿'
|
||||
},
|
||||
|
||||
/** 跳转订单详情 */
|
||||
goOrderDetail(orderId) {
|
||||
if (!orderId) return
|
||||
uni.navigateTo({ url: `/pages/order/order-detail?id=${orderId}` })
|
||||
},
|
||||
|
||||
formatTime(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
|
|
@ -196,4 +211,23 @@ export default {
|
|||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.view-order-btn {
|
||||
padding: 10rpx 28rpx;
|
||||
border: 1rpx solid #FFB700;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.view-order-btn text {
|
||||
font-size: 24rpx;
|
||||
color: #FFB700;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,14 @@ export default {
|
|||
getEarnings(),
|
||||
getWithdrawals()
|
||||
])
|
||||
this.earnings = earningsRes || this.earnings
|
||||
if (earningsRes) {
|
||||
this.earnings = {
|
||||
frozen: (earningsRes.frozenAmount || 0).toFixed(2),
|
||||
available: (earningsRes.availableAmount || 0).toFixed(2),
|
||||
withdrawing: (earningsRes.withdrawingAmount || 0).toFixed(2),
|
||||
withdrawn: (earningsRes.withdrawnAmount || 0).toFixed(2)
|
||||
}
|
||||
}
|
||||
this.withdrawals = withdrawalsRes?.items || withdrawalsRes || []
|
||||
} catch (e) {
|
||||
// 静默处理
|
||||
|
|
|
|||
|
|
@ -154,8 +154,7 @@ export default {
|
|||
goAgreement() { uni.navigateTo({ url: '/pages/config/agreement' }) },
|
||||
goPrivacy() { uni.navigateTo({ url: '/pages/config/privacy' }) },
|
||||
goRunnerAgreement() {
|
||||
// 跑腿协议,复用用户协议页面或单独页面
|
||||
uni.navigateTo({ url: '/pages/config/agreement' })
|
||||
uni.navigateTo({ url: '/pages/config/runner-agreement' })
|
||||
},
|
||||
/** 退出登录 */
|
||||
onLogout() {
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ export default {
|
|||
}
|
||||
|
||||
.confirm-ok {
|
||||
background-color: #007AFF;
|
||||
background-color: #FAD146;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
|
@ -462,7 +462,7 @@ export default {
|
|||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #007AFF;
|
||||
background-color: #FAD146;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
|
|
|
|||
|
|
@ -100,10 +100,13 @@
|
|||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 待确认:确认处理 -->
|
||||
<!-- 待确认:确认完成 + 订单未完成 + 查看详情 -->
|
||||
<template v-else-if="order.status === 'WaitConfirm'">
|
||||
<view class="btn btn-primary" @click="goConfirm(order)">
|
||||
<text>确认处理</text>
|
||||
<view class="btn btn-cancel" @click="onRejectComplete(order)">
|
||||
<text>订单未完成</text>
|
||||
</view>
|
||||
<view class="btn btn-primary" @click="onConfirmComplete(order)">
|
||||
<text>确认订单完成</text>
|
||||
</view>
|
||||
<view class="btn btn-detail" @click="goDetail(order)">
|
||||
<text>查看详情</text>
|
||||
|
|
@ -167,7 +170,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getMyOrders, cancelOrder, submitReview } from '../../utils/api'
|
||||
import { getMyOrders, cancelOrder, submitReview, confirmOrder, rejectOrder } from '../../utils/api'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -296,9 +299,36 @@ export default {
|
|||
uni.navigateTo({ url: `/pages/message/chat?orderId=${order.id}` })
|
||||
},
|
||||
|
||||
/** 跳转确认处理页 */
|
||||
goConfirm(order) {
|
||||
uni.navigateTo({ url: `/pages/order/complete-order?id=${order.id}&mode=confirm` })
|
||||
/** 单主确认订单完成 */
|
||||
onConfirmComplete(order) {
|
||||
uni.showModal({
|
||||
title: '确认完成',
|
||||
content: '确认该订单已完成?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await confirmOrder(order.id)
|
||||
uni.showToast({ title: '订单已完成', icon: 'success' })
|
||||
this.loadOrders()
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 单主拒绝订单完成 */
|
||||
onRejectComplete(order) {
|
||||
uni.showModal({
|
||||
title: '拒绝完成',
|
||||
content: '确认该订单未完成?订单将继续进行。',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await rejectOrder(order.id)
|
||||
uni.showToast({ title: '已拒绝,订单继续进行', icon: 'none' })
|
||||
this.loadOrders()
|
||||
} catch (e) {}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 打开评价弹窗 */
|
||||
|
|
@ -475,7 +505,7 @@ export default {
|
|||
}
|
||||
|
||||
.status-pending { color: #faad14; }
|
||||
.status-progress { color: #007AFF; }
|
||||
.status-progress { color: #FFB700; }
|
||||
.status-confirm { color: #ff9900; }
|
||||
.status-done { color: #52c41a; }
|
||||
.status-cancel { color: #999999; }
|
||||
|
|
|
|||
|
|
@ -153,27 +153,38 @@
|
|||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 进行中:联系跑腿 -->
|
||||
<!-- 进行中:单主看联系跑腿,跑腿看完成订单+联系单主 -->
|
||||
<template v-else-if="order.status === 'InProgress'">
|
||||
<view class="action-btn btn-primary" @click="goChat">
|
||||
<text>联系跑腿</text>
|
||||
<view v-if="!isOwner" class="action-btn btn-primary" @click="goCompleteOrder">
|
||||
<text>完成订单</text>
|
||||
</view>
|
||||
<view class="action-btn btn-secondary" @click="goChat">
|
||||
<text>{{ isOwner ? '联系跑腿' : '联系单主' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 已完成:评价跑腿 + 联系跑腿 -->
|
||||
<!-- 已完成:单主可评价+联系跑腿,跑腿可联系单主 -->
|
||||
<template v-else-if="order.status === 'Completed'">
|
||||
<view v-if="!order.isReviewed" class="action-btn btn-primary" @click="openReview">
|
||||
<view v-if="isOwner && !order.isReviewed" class="action-btn btn-primary" @click="openReview">
|
||||
<text>评价跑腿</text>
|
||||
</view>
|
||||
<view class="action-btn btn-secondary" @click="goChat">
|
||||
<text>联系跑腿</text>
|
||||
<text>{{ isOwner ? '联系跑腿' : '联系单主' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 待确认:确认处理 -->
|
||||
<!-- 待确认:单主看到确认/拒绝按钮,跑腿看到联系单主 -->
|
||||
<template v-else-if="order.status === 'WaitConfirm'">
|
||||
<view class="action-btn btn-primary" @click="goConfirm">
|
||||
<text>确认处理</text>
|
||||
<template v-if="isOwner">
|
||||
<view class="action-btn btn-cancel" @click="onRejectComplete">
|
||||
<text>订单未完成</text>
|
||||
</view>
|
||||
<view class="action-btn btn-primary" @click="onConfirmComplete">
|
||||
<text>确认订单完成</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="action-btn btn-secondary" @click="goChat">
|
||||
<text>{{ isOwner ? '联系跑腿' : '联系单主' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
|
@ -207,7 +218,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getOrderDetail, cancelOrder, submitReview } from '../../utils/api'
|
||||
import { getOrderDetail, cancelOrder, submitReview, confirmOrder, rejectOrder } from '../../utils/api'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
|
||||
export default {
|
||||
|
|
@ -225,6 +236,11 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
/** 当前用户是否为单主 */
|
||||
isOwner() {
|
||||
const userStore = useUserStore()
|
||||
return this.order.ownerId === userStore.userId
|
||||
},
|
||||
/** 按门店分组美食街菜品 */
|
||||
groupedFoodItems() {
|
||||
const groups = {}
|
||||
|
|
@ -324,9 +340,45 @@ export default {
|
|||
uni.navigateTo({ url: `/pages/message/chat?orderId=${this.orderId}` })
|
||||
},
|
||||
|
||||
/** 跳转确认处理页 */
|
||||
goConfirm() {
|
||||
uni.navigateTo({ url: `/pages/order/complete-order?id=${this.orderId}&mode=confirm` })
|
||||
/** 跳转完成订单页(跑腿提交完成凭证) */
|
||||
goCompleteOrder() {
|
||||
uni.navigateTo({ url: `/pages/order/complete-order?id=${this.orderId}` })
|
||||
},
|
||||
|
||||
/** 单主确认订单完成 */
|
||||
onConfirmComplete() {
|
||||
uni.showModal({
|
||||
title: '确认完成',
|
||||
content: '确认该订单已完成?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await confirmOrder(this.orderId)
|
||||
uni.showToast({ title: '订单已完成', icon: 'success' })
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
// 错误已在 request 中处理
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 单主拒绝订单完成 */
|
||||
onRejectComplete() {
|
||||
uni.showModal({
|
||||
title: '拒绝完成',
|
||||
content: '确认该订单未完成?订单将继续进行。',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await rejectOrder(this.orderId)
|
||||
uni.showToast({ title: '已拒绝,订单继续进行', icon: 'none' })
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
// 错误已在 request 中处理
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 打开评价弹窗 */
|
||||
|
|
@ -433,7 +485,7 @@ export default {
|
|||
}
|
||||
|
||||
.status-pending .status-text { color: #faad14; }
|
||||
.status-progress .status-text { color: #007AFF; }
|
||||
.status-progress .status-text { color: #FFB700; }
|
||||
.status-confirm .status-text { color: #ff9900; }
|
||||
.status-done .status-text { color: #52c41a; }
|
||||
.status-cancel .status-text { color: #999999; }
|
||||
|
|
@ -679,7 +731,7 @@ export default {
|
|||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
background-color: #007AFF;
|
||||
background-color: #FAD146;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -192,6 +192,11 @@ export function getPrivacy() {
|
|||
return request({ url: '/api/config/privacy' })
|
||||
}
|
||||
|
||||
/** 获取跑腿协议 */
|
||||
export function getRunnerAgreement() {
|
||||
return request({ url: '/api/config/runner-agreement' })
|
||||
}
|
||||
|
||||
/** 获取提现说明 */
|
||||
export function getWithdrawalGuide() {
|
||||
return request({ url: '/api/config/withdrawal-guide' })
|
||||
|
|
|
|||
|
|
@ -91,6 +91,49 @@ export function getConversationId(targetUserId) {
|
|||
return `C2C${targetUserId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表(用于消息页展示聊天记录)
|
||||
* @returns {Promise<Array>} 会话列表
|
||||
*/
|
||||
export async function getConversationList() {
|
||||
if (!chat || !isReady) return []
|
||||
try {
|
||||
const res = await chat.getConversationList()
|
||||
const list = res.data.conversationList || []
|
||||
// 只返回 C2C 单聊会话
|
||||
return list
|
||||
.filter(c => c.type === TencentCloudChat.TYPES.CONV_C2C)
|
||||
.map(c => {
|
||||
const lastMsg = c.lastMessage
|
||||
let lastMessageText = ''
|
||||
if (lastMsg) {
|
||||
if (lastMsg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
||||
lastMessageText = lastMsg.payload?.text || ''
|
||||
} else if (lastMsg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
||||
lastMessageText = '[图片]'
|
||||
} else if (lastMsg.type === TencentCloudChat.TYPES.MSG_CUSTOM) {
|
||||
lastMessageText = lastMsg.payload?.description || '[自定义消息]'
|
||||
} else {
|
||||
lastMessageText = '[消息]'
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationID: c.conversationID,
|
||||
targetUserId: c.userProfile?.userID || c.conversationID.replace('C2C', ''),
|
||||
nickname: c.userProfile?.nick || c.conversationID.replace('C2C', ''),
|
||||
avatarUrl: c.userProfile?.avatar || '',
|
||||
lastMessage: lastMessageText,
|
||||
lastMessageTime: lastMsg ? lastMsg.lastTime * 1000 : 0,
|
||||
unreadCount: c.unreadCount || 0
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.lastMessageTime - a.lastMessageTime)
|
||||
} catch (e) {
|
||||
console.error('[IM] 获取会话列表失败:', e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取历史消息
|
||||
* @param {string} targetUserId - 对方用户 ID
|
||||
|
|
|
|||
907
server/Migrations/20260320094803_AddWithdrawalProcessedAt.Designer.cs
generated
Normal file
907
server/Migrations/20260320094803_AddWithdrawalProcessedAt.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,907 @@
|
|||
// <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("20260320094803_AddWithdrawalProcessedAt")]
|
||||
partial class AddWithdrawalProcessedAt
|
||||
{
|
||||
/// <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.HasKey("Id");
|
||||
|
||||
b.HasIndex("OpenId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Phone");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("ProcessedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QrCodeImage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Withdrawals");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Appeal", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Dish", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany("Dishes")
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Earning", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.FoodOrderItem", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Dish", "Dish")
|
||||
.WithMany()
|
||||
.HasForeignKey("DishId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany("FoodOrderItems")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany()
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dish");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.MessageRead", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Order", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "Runner")
|
||||
.WithMany()
|
||||
.HasForeignKey("RunnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("Runner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.PriceChange", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "Initiator")
|
||||
.WithMany()
|
||||
.HasForeignKey("InitiatorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Initiator");
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Review", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CampusErrand.Models.User", "Runner")
|
||||
.WithMany()
|
||||
.HasForeignKey("RunnerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("Runner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.RunnerCertification", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.ShopBanner", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.Shop", "Shop")
|
||||
.WithMany("ShopBanners")
|
||||
.HasForeignKey("ShopId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shop");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Withdrawal", b =>
|
||||
{
|
||||
b.HasOne("CampusErrand.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Order", b =>
|
||||
{
|
||||
b.Navigation("FoodOrderItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CampusErrand.Models.Shop", b =>
|
||||
{
|
||||
b.Navigation("Dishes");
|
||||
|
||||
b.Navigation("ShopBanners");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
29
server/Migrations/20260320094803_AddWithdrawalProcessedAt.cs
Normal file
29
server/Migrations/20260320094803_AddWithdrawalProcessedAt.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CampusErrand.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWithdrawalProcessedAt : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ProcessedAt",
|
||||
table: "Withdrawals",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProcessedAt",
|
||||
table: "Withdrawals");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -697,6 +697,9 @@ namespace CampusErrand.Migrations
|
|||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("ProcessedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("QrCodeImage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
|
|
|
|||
|
|
@ -66,3 +66,13 @@ public class WithdrawRequest
|
|||
[Required(ErrorMessage = "收款二维码不能为空")]
|
||||
public string QrCodeImage { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理端提现审核请求
|
||||
/// </summary>
|
||||
public class AdminWithdrawalRequest
|
||||
{
|
||||
/// <summary>操作:approve(通过)、reject(拒绝)、processing(处理中)</summary>
|
||||
[Required(ErrorMessage = "操作类型不能为空")]
|
||||
public string Action { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ public class OrderResponse
|
|||
public string? RunnerNickname { get; set; }
|
||||
/// <summary>跑腿 UID</summary>
|
||||
public int? RunnerUid { get; set; }
|
||||
/// <summary>跑腿手机号(认证时填写的手机号,仅单主可见)</summary>
|
||||
public string? RunnerPhone { get; set; }
|
||||
public List<FoodOrderItemResponse>? FoodItems { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ public class Withdrawal
|
|||
/// <summary>申请时间</summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>处理时间</summary>
|
||||
public DateTime? ProcessedAt { get; set; }
|
||||
|
||||
// 导航属性
|
||||
[ForeignKey(nameof(UserId))]
|
||||
public User? User { get; set; }
|
||||
|
|
|
|||
|
|
@ -721,6 +721,17 @@ app.MapGet("/api/orders/{id}", async (int id, HttpContext httpContext, AppDbCont
|
|||
runnerUid = order.RunnerId;
|
||||
}
|
||||
|
||||
// 跑腿手机号:从认证记录中获取,仅单主可见
|
||||
string? runnerPhone = null;
|
||||
if (currentUserId == order.OwnerId && order.RunnerId != null)
|
||||
{
|
||||
var cert = await db.RunnerCertifications
|
||||
.Where(c => c.UserId == order.RunnerId && c.Status == CertificationStatus.Approved)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
runnerPhone = cert?.Phone;
|
||||
}
|
||||
|
||||
if (order.Status == OrderStatus.Completed || order.Status == OrderStatus.WaitConfirm)
|
||||
{
|
||||
// 已完成和待确认状态显示完成时间和凭证
|
||||
|
|
@ -750,7 +761,8 @@ app.MapGet("/api/orders/{id}", async (int id, HttpContext httpContext, AppDbCont
|
|||
AcceptedAt = visibleAcceptedAt,
|
||||
CompletedAt = visibleCompletedAt,
|
||||
RunnerNickname = runnerNickname,
|
||||
RunnerUid = runnerUid
|
||||
RunnerUid = runnerUid,
|
||||
RunnerPhone = runnerPhone
|
||||
};
|
||||
|
||||
// 美食街订单附带菜品详情
|
||||
|
|
@ -2119,6 +2131,118 @@ app.MapPost("/api/earnings/withdraw", async (WithdrawRequest request, HttpContex
|
|||
});
|
||||
}).RequireAuthorization();
|
||||
|
||||
// ========== 管理端提现审核接口 ==========
|
||||
|
||||
// 管理端获取提现列表
|
||||
app.MapGet("/api/admin/withdrawals", async (string? status, AppDbContext db) =>
|
||||
{
|
||||
var query = db.Withdrawals
|
||||
.Include(w => w.User)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(status) && Enum.TryParse<WithdrawalStatus>(status, out var s))
|
||||
query = query.Where(w => w.Status == s);
|
||||
|
||||
var list = await query
|
||||
.OrderByDescending(w => w.CreatedAt)
|
||||
.Select(w => new
|
||||
{
|
||||
w.Id,
|
||||
w.UserId,
|
||||
UserNickname = w.User!.Nickname ?? ("用户" + w.UserId),
|
||||
w.Amount,
|
||||
PaymentMethod = w.PaymentMethod.ToString(),
|
||||
w.QrCodeImage,
|
||||
Status = w.Status.ToString(),
|
||||
w.CreatedAt,
|
||||
w.ProcessedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(list);
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// 管理端审核提现(通过/拒绝)
|
||||
app.MapPut("/api/admin/withdrawals/{id}", async (int id, AdminWithdrawalRequest request, AppDbContext db) =>
|
||||
{
|
||||
var withdrawal = await db.Withdrawals.FindAsync(id);
|
||||
if (withdrawal == null)
|
||||
return Results.NotFound(new { code = 404, message = "提现记录不存在" });
|
||||
|
||||
if (withdrawal.Status != WithdrawalStatus.Pending && withdrawal.Status != WithdrawalStatus.Processing)
|
||||
return Results.BadRequest(new { code = 400, message = "该提现记录已处理完毕" });
|
||||
|
||||
if (request.Action == "approve")
|
||||
{
|
||||
withdrawal.Status = WithdrawalStatus.Completed;
|
||||
withdrawal.ProcessedAt = DateTime.UtcNow;
|
||||
|
||||
// 将对应收益标记为已提现
|
||||
var earnings = await db.Earnings
|
||||
.Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing)
|
||||
.OrderBy(e => e.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var remaining = withdrawal.Amount;
|
||||
foreach (var earning in earnings)
|
||||
{
|
||||
if (remaining <= 0) break;
|
||||
if (earning.NetEarning <= remaining)
|
||||
{
|
||||
earning.Status = EarningStatus.Withdrawn;
|
||||
remaining -= earning.NetEarning;
|
||||
}
|
||||
else
|
||||
{
|
||||
earning.Status = EarningStatus.Withdrawn;
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (request.Action == "reject")
|
||||
{
|
||||
withdrawal.Status = WithdrawalStatus.Pending;
|
||||
withdrawal.ProcessedAt = DateTime.UtcNow;
|
||||
|
||||
// 将对应收益退回待提现状态
|
||||
var earnings = await db.Earnings
|
||||
.Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing)
|
||||
.OrderBy(e => e.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var remaining = withdrawal.Amount;
|
||||
foreach (var earning in earnings)
|
||||
{
|
||||
if (remaining <= 0) break;
|
||||
if (earning.NetEarning <= remaining)
|
||||
{
|
||||
earning.Status = EarningStatus.Available;
|
||||
remaining -= earning.NetEarning;
|
||||
}
|
||||
else
|
||||
{
|
||||
earning.Status = EarningStatus.Available;
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝后将提现记录状态设为特殊标记(复用 Pending 但已有 ProcessedAt)
|
||||
// 实际上拒绝后应该删除或标记,这里直接删除提现记录
|
||||
db.Withdrawals.Remove(withdrawal);
|
||||
}
|
||||
else if (request.Action == "processing")
|
||||
{
|
||||
withdrawal.Status = WithdrawalStatus.Processing;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.BadRequest(new { code = 400, message = "无效操作,可选: approve, reject, processing" });
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Ok(new { message = "操作成功" });
|
||||
}).RequireAuthorization("AdminOnly");
|
||||
|
||||
// ========== 佣金规则接口 ==========
|
||||
|
||||
// 获取佣金规则
|
||||
|
|
@ -2369,13 +2493,13 @@ app.MapPost("/api/admin/notifications", async (CreateNotificationRequest request
|
|||
// 管理端获取跑腿列表
|
||||
app.MapGet("/api/admin/runners", async (AppDbContext db) =>
|
||||
{
|
||||
var runners = await db.Users
|
||||
.Where(u => db.RunnerCertifications.Any(c => c.UserId == u.Id && c.Status == CertificationStatus.Approved))
|
||||
.Select(u => new
|
||||
var runners = await db.RunnerCertifications
|
||||
.Where(c => c.Status == CertificationStatus.Approved)
|
||||
.Join(db.Users, c => c.UserId, u => u.Id, (c, u) => new
|
||||
{
|
||||
u.Id,
|
||||
u.Nickname,
|
||||
u.Phone,
|
||||
Phone = c.Phone, // 使用认证表中的手机号
|
||||
u.RunnerScore,
|
||||
u.IsBanned,
|
||||
u.CreatedAt
|
||||
|
|
@ -2638,6 +2762,18 @@ app.MapGet("/api/config/privacy", async (AppDbContext db) =>
|
|||
});
|
||||
});
|
||||
|
||||
// 获取跑腿协议
|
||||
app.MapGet("/api/config/runner-agreement", async (AppDbContext db) =>
|
||||
{
|
||||
var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "runner_agreement");
|
||||
return Results.Ok(new ConfigResponse
|
||||
{
|
||||
Key = "runner_agreement",
|
||||
Value = config?.Value ?? "",
|
||||
UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue
|
||||
});
|
||||
});
|
||||
|
||||
// 获取提现说明
|
||||
app.MapGet("/api/config/withdrawal-guide", async (AppDbContext db) =>
|
||||
{
|
||||
|
|
@ -2668,7 +2804,7 @@ app.MapPut("/api/admin/config/{key}", async (string key, UpdateConfigRequest req
|
|||
// 允许的配置键白名单
|
||||
var allowedKeys = new HashSet<string>
|
||||
{
|
||||
"qrcode", "agreement", "privacy", "withdrawal_guide", "freeze_days",
|
||||
"qrcode", "agreement", "privacy", "runner_agreement", "withdrawal_guide", "freeze_days",
|
||||
"page_banner_pickup", "page_banner_delivery", "page_banner_help", "page_banner_purchase", "page_banner_food",
|
||||
"page_banner_order-hall"
|
||||
};
|
||||
|
|
@ -2750,6 +2886,26 @@ using (var scope = app.Services.CreateScope())
|
|||
}
|
||||
}
|
||||
|
||||
// 注册后台定时任务:每10分钟执行一次自动确认和解冻
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(10));
|
||||
try
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await AutoConfirmExpiredOrders(db);
|
||||
await UnfreezeEarnings(db);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[定时任务] 执行失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
// Banner 请求校验辅助方法
|
||||
|
|
@ -2880,5 +3036,50 @@ static async Task UnfreezeEarnings(AppDbContext db)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动确认超过24小时未处理的待确认订单
|
||||
/// </summary>
|
||||
static async Task AutoConfirmExpiredOrders(AppDbContext db)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddHours(-24);
|
||||
var expiredOrders = await db.Orders
|
||||
.Where(o => o.Status == OrderStatus.WaitConfirm && o.CompletedAt != null && o.CompletedAt <= cutoff)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var order in expiredOrders)
|
||||
{
|
||||
order.Status = OrderStatus.Completed;
|
||||
|
||||
// 计算佣金收益
|
||||
var rules = await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync();
|
||||
var platformFee = CalculatePlatformFee(order.Commission, rules);
|
||||
var netEarning = order.Commission - platformFee;
|
||||
|
||||
var freezeDaysConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "freeze_days");
|
||||
var freezeDays = 1;
|
||||
if (freezeDaysConfig != null && int.TryParse(freezeDaysConfig.Value, out var configDays))
|
||||
freezeDays = configDays;
|
||||
|
||||
db.Earnings.Add(new Earning
|
||||
{
|
||||
UserId = order.RunnerId!.Value,
|
||||
OrderId = order.Id,
|
||||
GoodsAmount = order.GoodsAmount,
|
||||
Commission = order.Commission,
|
||||
PlatformFee = platformFee,
|
||||
NetEarning = netEarning,
|
||||
Status = EarningStatus.Frozen,
|
||||
FrozenUntil = DateTime.UtcNow.AddDays(freezeDays),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
if (expiredOrders.Count > 0)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
Console.WriteLine($"[定时任务] 自动确认了 {expiredOrders.Count} 个超时订单");
|
||||
}
|
||||
}
|
||||
|
||||
// 用于集成测试访问
|
||||
public partial class Program { }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user