feat: 快速派单功能分离 & 视频上传功能修复 & 考试功能配置优化

This commit is contained in:
你的名字 2026-02-26 19:45:49 +08:00
commit 8867d27f70
8 changed files with 8292 additions and 0 deletions

View File

@ -0,0 +1,127 @@
package com.peidu.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.peidu.context.TenantContext;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* MyBatis Plus配置
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 租户插件 + 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 租户插件必须在分页插件之前
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
Long tenantId = TenantContext.getTenantId();
// 如果租户ID为null返回默认租户ID 1
// 避免生成 WHERE tenant_id = NULL 的错误SQL
if (tenantId == null) {
return new LongValue(1L);
}
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 忽略不需要租户过滤的表
List<String> ignoreTables = Arrays.asList(
// 系统管理表
"admin", "role", "permission", "role_permission", "admin_role",
"system_config", "operation_log",
// 租户管理表
"tenant", "tenant_admin", "tenant_config", "tenant_package",
"tenant_order", "tenant_statistics",
// 加盟管理表
"franchise_application", "franchise_follow_record",
"franchise_policy", "franchise_contract", "franchise_payment",
// 全局数据表所有租户共享
"banner", "service_category", "announcement",
"parent_academy_course", "summer_camp", "growth_planning_service",
"tutoring_service", "study_tour", "special_course",
"interest_course", "online_supervision", "assessment_service",
// 用户相关表测试环境忽略租户过滤
"user", "parent", "student", "teacher", "teacher_schedule",
"manager", "provider", "schedule", "checkin_record",
// 服务和订单相关表测试环境忽略租户过滤
"service", "training_course", "`order`", "order", "payment_record",
"check_in_record", "learning_record", "review",
"work_order", "growth_record",
// 营销和活动相关表测试环境忽略租户过滤
"coupon", "user_coupon", "marketing_activity", "activity", "group_buying",
"group_buying_team", "group_buying_member",
// 时卡和套餐相关表测试环境忽略租户过滤
"time_card", "time_card_usage", "package", "user_package",
// 积分和钱包相关表测试环境忽略租户过滤
"points_record", "wallet", "wallet_record", "withdraw_record",
"wallet_transaction",
// 通知和反馈相关表测试环境忽略租户过滤
"notification", "feedback", "sms_record", "announcement",
// 提醒管理表测试环境忽略租户过滤
"reminder",
// 提现相关表
"withdraw",
"teacher_video",
// 陪伴员等级/考核相关表避免缺少 tenant_id 时出现 SQL 异常
"teacher_level",
"exam_question",
"exam_record",
"exam_answer",
// 协议和物资相关表
"agreement", "material",
// 客服相关表
"customer_service_staff", "customer_service_record",
// 分销相关表测试环境忽略租户过滤
"distributor", "distributor_commission", "distributor_withdraw"
);
return ignoreTables.contains(tableName.toLowerCase());
}
}));
// 2. 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

File diff suppressed because it is too large Load Diff

1078
peidu/uniapp/pages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<template>
<view class="container">
<!-- 管理师派单界面 -->
<ManagerBooking />
</view>
</template>
<script>
import ManagerBooking from '@/pages/booking/components/ManagerBooking.vue'
export default {
components: {
ManagerBooking
}
}
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
background: #f8f8f8;
}
</style>

1466
peidu/uniapp/src/pages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,513 @@
<template>
<view class="manager-booking">
<view class="header">
<view class="back-button" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="header-content">
<text class="title">👔 管理师派单</text>
<text class="subtitle">管理订单和陪伴员</text>
</view>
</view>
<!-- 数据统计 -->
<view class="stats-section">
<view class="stat-card">
<text class="stat-value">{{ stats.pending }}</text>
<text class="stat-label">待派单</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.processing }}</text>
<text class="stat-label">进行中</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.teachers }}</text>
<text class="stat-label">陪伴员</text>
</view>
</view>
<!-- 待派单列表 -->
<view class="order-section">
<view class="section-header">
<text class="section-title">待派单订单</text>
<text class="section-tip">选择合适的陪伴员</text>
</view>
<view v-if="orderList.length === 0" class="empty-state">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无待派单订单</text>
</view>
<view v-else class="order-list">
<view
v-for="order in orderList"
:key="order.id"
class="order-card"
>
<view class="order-header">
<text class="order-title">{{ order.serviceName }}</text>
<text class="order-status">待派单</text>
</view>
<view class="order-info">
<view class="info-item">
<text class="info-label">服务时间</text>
<text class="info-value">{{ order.serviceTime }}</text>
</view>
<view class="info-item">
<text class="info-label">服务地址</text>
<text class="info-value">{{ order.address }}</text>
</view>
<view class="info-item">
<text class="info-label">孩子信息</text>
<text class="info-value">{{ order.childInfo }}</text>
</view>
<view class="info-item">
<text class="info-label">家长需求</text>
<text class="info-value">{{ order.requirement }}</text>
</view>
</view>
<view class="order-footer">
<button class="btn-assign" @click="assignOrder(order)">派单</button>
<button class="btn-detail" @click="viewDetail(order)">详情</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { useUserStore } from '@/store/user'
import { managerApi } from '@/api/index.js'
export default {
data() {
return {
userStore: useUserStore(),
managerId: null,
stats: {
pending: 0,
processing: 0,
teachers: 0
},
orderList: [],
loading: false
}
},
mounted() {
console.log('[ManagerBooking] ========== mounted 开始 ==========')
console.log('[ManagerBooking] 当前时间:', new Date().toLocaleString())
this.initManagerId()
this.loadData()
// 🔥
uni.$on('refreshManagerBooking', this.handleRefresh)
console.log('[ManagerBooking] ========== mounted 完成 ==========')
},
activated() {
console.log('[ManagerBooking] ========== activated 开始 ==========')
console.log('[ManagerBooking] 页面激活,重新加载数据')
this.loadData()
console.log('[ManagerBooking] ========== activated 完成 ==========')
},
beforeDestroy() {
//
uni.$off('refreshManagerBooking', this.handleRefresh)
},
methods: {
initManagerId() {
const userInfo = this.userStore.userInfo || uni.getStorageSync('userInfo')
console.log('[ManagerBooking] 🔍 用户信息:', userInfo)
if (userInfo) {
// 🔥 ID
this.managerId = userInfo.id || userInfo.userId || userInfo.user_id
console.log('[ManagerBooking] 🔍 managerId:', this.managerId)
console.log('[ManagerBooking] 🔍 managerId 类型:', typeof this.managerId)
}
if (!this.managerId) {
console.error('[ManagerBooking] ❌ 无法获取管理员ID')
console.error('[ManagerBooking] ❌ userInfo:', JSON.stringify(userInfo, null, 2))
// 🔥 使 managerId
console.warn('[ManagerBooking] ⚠️ 使用默认 managerId: 1')
this.managerId = 1
uni.showToast({ title: '使用默认管理员ID', icon: 'none', duration: 1500 })
}
},
async loadData() {
if (!this.managerId) {
console.error('[ManagerBooking] managerId 未设置')
return
}
console.log('[ManagerBooking] ========== loadData 开始 ==========')
// 🔥 使 getStatistics
await Promise.all([
this.loadStatistics(), //
this.loadPendingOrders() //
])
console.log('[ManagerBooking] 🔍 loadData 完成后 stats:', JSON.stringify(this.stats))
console.log('[ManagerBooking] ========== loadData 完成 ==========')
},
async loadStatistics() {
try {
const managerId = Number(this.managerId)
console.log('[ManagerBooking] 调用 getStatistics, managerId:', managerId)
const res = await managerApi.getStatistics(managerId)
console.log('[ManagerBooking] getStatistics 响应:', res)
if (res.code === 200 && res.data) {
// 🔥 使 getStatistics
this.stats.pending = res.data.pendingOrders || 0
this.stats.processing = res.data.processingOrders || 0
this.stats.teachers = res.data.activeTeachers || 0
console.log('[ManagerBooking] 统计数据已更新:', this.stats)
}
} catch (error) {
console.error('[ManagerBooking] 加载统计数据失败:', error)
}
},
async loadPendingOrders() {
if (this.loading) return
this.loading = true
try {
// 🔥
const params = {
page: 1,
size: 1000, // 🔥
dispatchStatus: 'pending' // teacher_id null
}
console.log('[ManagerBooking] 📤 请求参数:', JSON.stringify(params))
const res = await managerApi.getWorkOrders(params)
console.log('[ManagerBooking] 📥 API原始响应:', JSON.stringify(res, null, 2))
// 🔥 使
let records = []
if (res) {
if (res.code === 200 && res.data) {
records = res.data.records || []
console.log('[ManagerBooking] ✅ 数据格式:标准格式')
} else if (res.records) {
records = res.records
console.log('[ManagerBooking] ✅ 数据格式:分页对象')
} else if (Array.isArray(res)) {
records = res
console.log('[ManagerBooking] ✅ 数据格式:数组')
}
}
console.log('[ManagerBooking] 📊 解析后的记录数:', records.length)
// 🔥 status=0 payStatus=1
// status=0 status=1 /
const pendingRecords = records.filter(item => {
const isPending = item.status === 0 && item.payStatus === 1 && !item.teacherId
if (!isPending) {
console.log('[ManagerBooking] 🚫 过滤掉非待派单订单:', item.id, 'status:', item.status, 'payStatus:', item.payStatus, 'teacherId:', item.teacherId)
}
return isPending
})
console.log('[ManagerBooking] ✅ 过滤后的待派单订单数:', pendingRecords.length)
this.orderList = pendingRecords.map(order => ({
id: order.id,
serviceName: order.title || order.serviceName || '服务订单',
serviceTime: this.formatServiceTime(order),
address: order.serviceAddress || '待确认',
childInfo: this.formatChildInfo(order),
requirement: order.content || order.requirement || '无特殊要求'
}))
console.log('[ManagerBooking] ========== 订单列表加载完成 ==========')
console.log('[ManagerBooking] 订单列表长度:', this.orderList.length)
console.log('[ManagerBooking] ⚠️ 不更新 stats.pending使用 getStatistics 接口数据')
} catch (error) {
console.error('[ManagerBooking] 加载待派单订单失败:', error)
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
this.loading = false
}
},
formatServiceTime(order) {
//
if (order.serviceDate && order.timeSlot) {
return `${order.serviceDate} ${order.timeSlot}`
}
if (order.createTime) {
return order.createTime
}
return '待确认'
},
formatChildInfo(order) {
//
if (order.studentName) {
let info = order.studentName
if (order.studentAge) {
info += `${order.studentAge}`
}
if (order.studentGrade) {
info += `${order.studentGrade}`
}
return info
}
return '待确认'
},
handleRefresh() {
console.log('[ManagerBooking] 收到刷新事件')
if (this.managerId) {
this.loadData()
} else {
this.initManagerId()
this.loadData()
}
},
assignOrder(order) {
uni.navigateTo({
url: `/manager-package/pages/manager/assign?orderId=${order.id}`
})
},
viewDetail(order) {
uni.navigateTo({
url: `/manager-package/pages/manager/work-order-detail?id=${order.id}`
})
},
goBack() {
//
uni.switchTab({
url: '/pages/index/index'
})
}
}
}
</script>
<style lang="scss" scoped>
@import '@/static/css/common.scss';
.manager-booking {
min-height: 100vh;
background: #f8f8f8;
padding-bottom: 40rpx;
}
.header {
background: linear-gradient(135deg, $primary-color 0%, $primary-light 100%);
padding: calc(50rpx + env(safe-area-inset-top)) 30rpx 40rpx;
display: flex;
align-items: center;
position: relative;
.back-button {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 30rpx;
z-index: 10;
.back-icon {
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
}
}
.header-content {
flex: 1;
text-align: center;
}
.title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 15rpx;
}
.subtitle {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
}
}
.stats-section {
display: flex;
gap: 20rpx;
padding: 30rpx;
.stat-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx 20rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.stat-value {
display: block;
font-size: 48rpx;
font-weight: bold;
color: $primary-color;
margin-bottom: 10rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666;
}
}
}
.order-section {
padding: 0 30rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.section-tip {
font-size: 24rpx;
color: #999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 60rpx;
background: #ffffff;
border-radius: 16rpx;
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 30rpx;
color: #333;
}
}
.order-list {
.order-card {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.order-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.order-status {
padding: 8rpx 20rpx;
background: #fff3cd;
color: #856404;
font-size: 22rpx;
border-radius: 20rpx;
}
}
.order-info {
margin-bottom: 20rpx;
.info-item {
display: flex;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #999;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #333;
}
}
}
.order-footer {
display: flex;
gap: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
.btn-assign {
flex: 1;
height: 70rpx;
background: linear-gradient(135deg, $primary-color 0%, $primary-light 100%);
color: #ffffff;
font-size: 28rpx;
border-radius: 35rpx;
border: none;
}
.btn-detail {
width: 140rpx;
height: 70rpx;
background: #ffffff;
color: $primary-color;
font-size: 28rpx;
border-radius: 35rpx;
border: 2rpx solid $primary-color;
}
}
}
}
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<view class="container">
<!-- 快速预约页面显示家长端预约界面 -->
<!-- 游客模式可浏览点击预约时拦截登录 -->
<ParentBooking :isGuestMode="!isLoggedIn" />
</view>
</template>
<script>
import { useUserStore } from '@/store/user'
import ParentBooking from './components/ParentBooking.vue'
import TeacherBooking from './components/TeacherBooking.vue'
import ManagerBooking from './components/ManagerBooking.vue'
import DistributorBooking from './components/DistributorBooking.vue'
import ProviderBooking from './components/ProviderBooking.vue'
import RoleSelection from './components/RoleSelection.vue'
export default {
components: {
ParentBooking,
TeacherBooking,
ManagerBooking,
DistributorBooking,
ProviderBooking,
RoleSelection
},
data() {
return {
isLoggedIn: false
}
},
computed: {
currentRole() {
const userStore = useUserStore()
return userStore.currentRole
},
roleName() {
const userStore = useUserStore()
return userStore.roleName
}
},
onShow() {
console.log('[快速预约页面] onShow 触发')
this.checkLoginStatus()
this.loadTabBarParams()
console.log('[快速预约页面] isLoggedIn:', this.isLoggedIn)
console.log('[快速预约页面] currentRole:', this.currentRole)
// 🔥
this.$nextTick(() => {
// 使线 ManagerBooking
uni.$emit('refreshManagerBooking')
})
},
methods: {
// tabBar
loadTabBarParams() {
//
const params = uni.getStorageSync('bookingQuickBookingParams')
if (params) {
console.log('[快速预约] 接收到参数:', params)
// 使
uni.removeStorageSync('bookingQuickBookingParams')
//
// 线
if (params.packageId) {
uni.$emit('bookingParams', { packageId: params.packageId })
}
if (params.serviceId) {
uni.$emit('bookingParams', { serviceId: params.serviceId })
}
if (params.teacherId) {
uni.$emit('bookingParams', { teacherId: params.teacherId })
}
}
},
//
checkLoginStatus() {
const token = uni.getStorageSync('token')
const userInfo = uni.getStorageSync('userInfo')
this.isLoggedIn = !!(token && userInfo)
},
// ""
goToLogin() {
uni.switchTab({
url: '/pages/user/index'
})
},
//
handleRoleSelect(role) {
const userStore = useUserStore()
userStore.switchRole(role)
}
}
}
</script>
<style lang="scss" scoped>
@import '@/static/css/common.scss';
.container {
min-height: 100vh;
background: #f8f8f8;
}
.login-prompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 120rpx 60rpx;
background: #ffffff;
.prompt-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.prompt-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
margin-bottom: 15rpx;
}
.prompt-desc {
font-size: 26rpx;
color: #666666;
margin-bottom: 50rpx;
text-align: center;
}
.login-btn {
width: 400rpx;
height: 88rpx;
background: linear-gradient(135deg, $primary-color 0%, $primary-light 100%);
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 44rpx;
border: none;
}
}
</style>

File diff suppressed because it is too large Load Diff