feat: 陪学项目功能优化 - 2026-03-04

This commit is contained in:
xiaojiang 2026-03-04 19:29:01 +08:00
parent 58df75a39f
commit 47101329e1
6 changed files with 341 additions and 131 deletions

View File

@ -30,9 +30,9 @@ spring:
maintainTimeStats: false
redis:
host: ${REDIS_HOST:your-prod-redis-host}
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD}
password: ${REDIS_PASSWORD:}
database: 0
timeout: 3000
lettuce:
@ -45,10 +45,11 @@ spring:
# JWT配置 - 生产环境使用环境变量
jwt:
secret: ${JWT_SECRET:peidu-secret-key-2026-change-this-in-production-min-256-bits} # 默认值
expiration: 604800 # 7天(秒)
secret: ${JWT_SECRET:peiduSecretKeyForJwtTokenGenerationMustBeAtLeast256BitsLongToWorkProperly} # 默认值与dev保持一致
expiration: 604800000 # 7天(秒)
header: Authorization
prefix: Bearer
prefix: Bearer
refresh-time: 518400000 # 6天(毫秒)
# 文件上传配置 - 生产环境
file:
@ -60,15 +61,15 @@ file:
# 微信小程序配置 - 生产环境
wx:
appid: ${WX_APPID} # 通过环境变量设置
secret: ${WX_SECRET} # 通过环境变量设置
appid: ${WX_APPID:your-wx-appid} # 通过环境变量设置
secret: ${WX_SECRET:your-wx-secret} # 通过环境变量设置
# 微信支付配置 - 生产环境
wechat:
pay:
appid: ${WX_PAY_APPID}
mchid: ${WX_PAY_MCHID}
key: ${WX_PAY_KEY}
appid: ${WX_PAY_APPID:your-wx-pay-appid}
mchid: ${WX_PAY_MCHID:your-mchid}
key: ${WX_PAY_KEY:your-pay-key}
cert-path: /data/peidu/cert/apiclient_cert.p12
notify-url: https://your-domain.com/api/payment/notify/wechat
@ -83,21 +84,21 @@ sms:
region: ${ALIYUN_SMS_REGION:cn-hangzhou}
endpoint: ${ALIYUN_SMS_ENDPOINT:dysmsapi.aliyuncs.com}
provider: aliyun
access-key: ${ALIYUN_SMS_ACCESS_KEY}
access-secret: ${ALIYUN_SMS_ACCESS_SECRET}
access-key: ${ALIYUN_SMS_ACCESS_KEY:}
access-secret: ${ALIYUN_SMS_ACCESS_SECRET:}
sign-name: ${SMS_SIGN_NAME:陪读服务}
template:
verify-code: ${SMS_TEMPLATE_VERIFY_CODE}
order-notify: ${SMS_TEMPLATE_ORDER_NOTIFY}
verify-code: ${SMS_TEMPLATE_VERIFY_CODE:}
order-notify: ${SMS_TEMPLATE_ORDER_NOTIFY:}
# 阿里云OSS配置 - 生产环境
aliyun:
oss:
endpoint: ${ALIYUN_OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
access-key-id: ${ALIYUN_OSS_ACCESS_KEY}
access-key-secret: ${ALIYUN_OSS_ACCESS_SECRET}
bucket-name: ${ALIYUN_OSS_BUCKET}
domain: ${ALIYUN_OSS_DOMAIN}
access-key-id: ${ALIYUN_OSS_ACCESS_KEY:}
access-key-secret: ${ALIYUN_OSS_ACCESS_SECRET:}
bucket-name: ${ALIYUN_OSS_BUCKET:}
domain: ${ALIYUN_OSS_DOMAIN:}
# 备份配置 - 生产环境
backup:

View File

@ -102,6 +102,24 @@
</picker>
</view>
<view class="form-item">
<text class="label">服务时段</text>
<view class="time-slot-row">
<picker :range="startTimeList" @change="onStartTimeChange">
<view class="time-picker">
{{ form.startTime || '开始时间' }}
</view>
</picker>
<text class="time-separator"></text>
<picker :range="endTimeList" @change="onEndTimeChange">
<view class="time-picker">
{{ form.endTime || '结束时间' }}
</view>
</picker>
</view>
<text class="time-duration" v-if="serviceDuration">预计服务时长{{ serviceDuration }}小时</text>
</view>
<view class="form-item">
<text class="label">其他需求</text>
<textarea class="textarea" v-model="form.remark" placeholder="请描述具体需求" />
@ -141,6 +159,8 @@ export default {
hobbies: '',
serviceTypes: [],
serviceDate: '',
startTime: '',
endTime: '',
remark: ''
},
gradeList: ['幼儿园', '一年级', '二年级', '三年级', '四年级', '五年级', '六年级', '初一', '初二', '初三', '高一', '高二', '高三'],
@ -151,7 +171,21 @@ export default {
{ label: '阅读陪伴', value: 'reading' },
{ label: '运动陪伴', value: 'sports' },
{ label: '其他', value: 'other' }
]
],
startTimeList: ['08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00'],
endTimeList: ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00']
}
},
computed: {
serviceDuration() {
if (!this.form.startTime || !this.form.endTime) {
return 0
}
const start = parseInt(this.form.startTime.split(':')[0])
const end = parseInt(this.form.endTime.split(':')[0])
const duration = end - start
return duration > 0 ? duration : 0
}
},
@ -172,6 +206,32 @@ export default {
this.form.serviceTypes = e.detail.value
},
onStartTimeChange(e) {
this.form.startTime = this.startTimeList[e.detail.value]
//
if (this.form.endTime) {
const start = parseInt(this.form.startTime.split(':')[0])
const end = parseInt(this.form.endTime.split(':')[0])
if (end <= start) {
this.form.endTime = ''
uni.showToast({ title: '请重新选择结束时间', icon: 'none' })
}
}
},
onEndTimeChange(e) {
this.form.endTime = this.endTimeList[e.detail.value]
//
if (this.form.startTime) {
const start = parseInt(this.form.startTime.split(':')[0])
const end = parseInt(this.form.endTime.split(':')[0])
if (end <= start) {
this.form.endTime = ''
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' })
}
}
},
async handleSubmit() {
// 🔥
if (this.isGuestMode) {
@ -236,8 +296,8 @@ export default {
serviceName: '快速预约-陪伴服务',
serviceType: this.form.serviceTypes.join(',') || '陪伴服务',
serviceDate: this.form.serviceDate || new Date().toISOString().split('T')[0],
timeSlot: '待定',
duration: 120, // 2
timeSlot: this.form.startTime && this.form.endTime ? `${this.form.startTime}-${this.form.endTime}` : '待定',
duration: this.serviceDuration * 60 || 120, // 2
serviceAddress: this.form.address || this.form.region,
price: 0, //
couponId: null,
@ -255,6 +315,8 @@ export default {
grade: this.form.grade,
hobbies: this.form.hobbies,
serviceTypes: this.form.serviceTypes,
startTime: this.form.startTime,
endTime: this.form.endTime,
remark: this.form.remark
}),
status: 0, //
@ -329,6 +391,8 @@ export default {
hobbies: '',
serviceTypes: [],
serviceDate: '',
startTime: '',
endTime: '',
remark: ''
}
}
@ -439,6 +503,37 @@ export default {
align-items: center;
}
.time-slot-row {
display: flex;
align-items: center;
gap: 20rpx;
.time-picker {
flex: 1;
height: 88rpx;
padding: 0 20rpx;
background: #f8f8f8;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #333;
}
.time-separator {
font-size: 28rpx;
color: #999;
}
}
.time-duration {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: $primary-color;
}
.radio-item, .checkbox-item {
display: inline-flex;
align-items: center;

View File

@ -155,7 +155,7 @@ export default {
{ id: 5, image: 'https://i.imglt.com/20260131/8fd69266fcf1cb035df071f9ce335bbb.jpg' }
],
// 4×3
// 4×3 + 51
functionIcons: [
//
{ id: 1, icon: 'https://i.imglt.com/20260201/dcc7793e837b1d4d38eeb58f1a18bf45.jpg', label: '陪伴员', path: '/pages/teacher/list' },
@ -170,7 +170,11 @@ export default {
{ id: 8, icon: 'https://i.imglt.com/20260201/c161a0ae0ae15e98fc851d6e4296718d.jpg', label: '专项提升', path: '/service-package/pages/special/list' },
{ id: 9, icon: 'https://i.imglt.com/20260201/939405d3dfae415df1a526dfb23e91ed.jpg', label: '兴趣培养', path: '/activity-package/pages/interest/category' },
//
{ id: 10, icon: 'https://i.imglt.com/20260201/9aa84915d9739f217a0c4185d26f751c.jpg', label: '线上督学', path: '/activity-package/pages/supervision/category' }
{ id: 10, icon: 'https://i.imglt.com/20260201/9aa84915d9739f217a0c4185d26f751c.jpg', label: '线上督学', path: '/activity-package/pages/supervision/category' },
{ id: 11, icon: 'https://i.imglt.com/20260201/55a5d2dea2a6376ae0c4372406465231.jpg', label: '热门课程', path: '/activity-package/pages/academy/index' },
{ id: 12, icon: 'https://i.imglt.com/20260201/dcc7793e837b1d4d38eeb58f1a18bf45.jpg', label: '优秀陪伴员', path: '/pages/teacher/list' },
//
{ id: 13, icon: 'https://i.imglt.com/20260201/c161a0ae0ae15e98fc851d6e4296718d.jpg', label: '专项课程', path: '/service-package/pages/special/list' }
],
//
@ -203,7 +207,7 @@ export default {
//
const [unreadCount, packages, homeCourses] = await Promise.all([
api.notificationApi.getUnreadCount().catch(() => ({ code: 200, data: 0 })),
api.packageApi.getPackageList({ page: 1, size: 10 }).catch(() => ({ records: [] })), // API
api.packageApi.getPackageList({ page: 1, size: 10 }).catch(() => ({ records: [] })),
api.homeApi.getHomeCourses({ hotLimit: 2, discountLimit: 2 }).catch(() => null)
])
@ -213,44 +217,28 @@ export default {
: 0
this.unreadCount = count
// ""
//
const packageList = packages.data?.records || packages.data?.list || packages.records || packages.list || packages || []
if (packageList.length > 0) {
this.specialPackages = packageList.slice(0, 2).map(pkg => ({
id: pkg.id,
title: pkg.packageName || '优惠套餐',
price: pkg.price ? pkg.price.toFixed(0) : '24900', // 使
price: pkg.price ? pkg.price.toFixed(0) : '24900',
originalPrice: pkg.originalPrice ? pkg.originalPrice.toFixed(0) : '49800',
desc: pkg.description || `${pkg.totalHours || 0}小时 · 有效期${pkg.validDays || 365}`,
type: 'package' //
type: 'package'
}))
} else {
// 使
this.specialPackages = [
{
id: 1,
title: '暖心式陪伴200小时',
price: '24900',
originalPrice: '49800',
desc: '200小时 · 有效期365天',
type: 'package'
},
{
id: 2,
title: '基准式陪伴100小时',
price: '12900',
originalPrice: '25800',
desc: '100小时 · 有效期365天',
type: 'package'
}
{ id: 1, title: '暖心式陪伴200小时', price: '24900', originalPrice: '49800', desc: '200小时 · 有效期365天', type: 'package' },
{ id: 2, title: '基准式陪伴100小时', price: '12900', originalPrice: '25800', desc: '100小时 · 有效期365天', type: 'package' }
]
}
// API
//
if (homeCourses && homeCourses.data) {
const courseData = homeCourses.data
//
if (courseData.hotCourses && courseData.hotCourses.length > 0) {
this.hotCourses = courseData.hotCourses.map((course, index) => ({
id: course.id,
@ -262,28 +250,12 @@ export default {
bgColor: this.getCourseBgColor(index)
}))
} else {
// 使
this.hotCourses = [
{
id: 1,
name: 'UI/UX设计实战课',
price: '129',
type: 'parent_academy',
iconText: '设计',
bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
id: 2,
name: 'Python编程入门',
price: '99',
type: 'parent_academy',
iconText: '编程',
bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
}
{ id: 1, name: 'UI/UX设计实战课', price: '129', type: 'parent_academy', iconText: '设计', bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ id: 2, name: 'Python编程入门', price: '99', type: 'parent_academy', iconText: '编程', bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }
]
}
//
if (courseData.discountCourses && courseData.discountCourses.length > 0) {
this.discountCourses = courseData.discountCourses.map((course, index) => ({
id: course.id,
@ -297,78 +269,24 @@ export default {
bgColor: this.getCourseBgColor(index)
}))
} else {
// 使
this.discountCourses = [
{
id: 1,
name: '小学数学提升班',
price: '399',
type: 'interest',
badge: '暑期特惠',
desc: '系统提升数学思维',
iconText: '数学',
bgColor: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
},
{
id: 2,
name: '初中英语语法班',
price: '499',
type: 'interest',
badge: '限时抢购',
desc: '掌握核心语法知识',
iconText: '英语',
bgColor: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)'
}
{ id: 1, name: '小学数学提升班', price: '399', type: 'interest', badge: '暑期特惠', desc: '系统提升数学思维', iconText: '数学', bgColor: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
{ id: 2, name: '初中英语语法班', price: '499', type: 'interest', badge: '限时抢购', desc: '掌握核心语法知识', iconText: '英语', bgColor: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' }
]
}
} else {
// API使
this.hotCourses = [
{
id: 1,
name: 'UI/UX设计实战课',
price: '129',
type: 'parent_academy',
iconText: '设计',
bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
id: 2,
name: 'Python编程入门',
price: '99',
type: 'parent_academy',
iconText: '编程',
bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
}
{ id: 1, name: 'UI/UX设计实战课', price: '129', type: 'parent_academy', iconText: '设计', bgColor: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ id: 2, name: 'Python编程入门', price: '99', type: 'parent_academy', iconText: '编程', bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }
]
this.discountCourses = [
{
id: 1,
name: '小学数学提升班',
price: '399',
type: 'interest',
badge: '暑期特惠',
desc: '系统提升数学思维',
iconText: '数学',
bgColor: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
},
{
id: 2,
name: '初中英语语法班',
price: '499',
type: 'interest',
badge: '限时抢购',
desc: '掌握核心语法知识',
iconText: '英语',
bgColor: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)'
}
{ id: 1, name: '小学数学提升班', price: '399', type: 'interest', badge: '暑期特惠', desc: '系统提升数学思维', iconText: '数学', bgColor: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
{ id: 2, name: '初中英语语法班', price: '499', type: 'interest', badge: '限时抢购', desc: '掌握核心语法知识', iconText: '英语', bgColor: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' }
]
}
} catch (error) {
console.error('加载首页数据失败:', error)
// 使
}
},

View File

@ -312,25 +312,162 @@ export default {
this.form.subject = this.subjects[this.subjectIndex]
},
//
uploadCover() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.form.coverImage = res.tempFilePaths[0]
// TODO:
const tempFilePath = res.tempFilePaths[0]
//
uni.showLoading({ title: '上传封面中...' })
//
uni.uploadFile({
url: `${getApp().globalData.baseUrl}/api/file/upload`,
filePath: tempFilePath,
name: 'file',
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) {
// URL使URL
this.form.coverImage = data.data.url || data.data.fileUrl
uni.hideLoading()
uni.showToast({
title: '封面上传成功',
icon: 'success',
duration: 1500
})
} else {
uni.hideLoading()
uni.showToast({
title: data.message || '上传失败',
icon: 'none',
duration: 2000
})
}
} catch (error) {
console.error('解析上传结果失败:', error)
uni.hideLoading()
uni.showToast({
title: '上传失败,请重试',
icon: 'none',
duration: 2000
})
}
},
fail: (err) => {
console.error('上传封面失败:', err)
uni.hideLoading()
uni.showToast({
title: '上传失败,请检查网络',
icon: 'none',
duration: 2000
})
}
})
},
fail: (err) => {
console.log('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none',
duration: 2000
})
}
})
},
//
uploadImages() {
const maxCount = 9 - this.form.images.length
if (maxCount <= 0) {
uni.showToast({
title: '最多上传9张图片',
icon: 'none',
duration: 2000
})
return
}
uni.chooseImage({
count: maxCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.form.images.push(...res.tempFilePaths)
// TODO:
const tempFilePaths = res.tempFilePaths
//
uni.showLoading({ title: `上传中 0/${tempFilePaths.length}` })
//
this.uploadImagesSequentially(tempFilePaths, 0, [])
},
fail: (err) => {
console.log('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none',
duration: 2000
})
}
})
},
//
uploadImagesSequentially(filePaths, index, uploadedUrls) {
if (index >= filePaths.length) {
//
uni.hideLoading()
this.form.images.push(...uploadedUrls)
uni.showToast({
title: `成功上传${uploadedUrls.length}张图片`,
icon: 'success',
duration: 2000
})
return
}
//
uni.showLoading({ title: `上传中 ${index + 1}/${filePaths.length}` })
//
uni.uploadFile({
url: `${getApp().globalData.baseUrl}/api/file/upload`,
filePath: filePaths[index],
name: 'file',
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) {
const imageUrl = data.data.url || data.data.fileUrl
uploadedUrls.push(imageUrl)
//
this.uploadImagesSequentially(filePaths, index + 1, uploadedUrls)
} else {
console.error(`${index + 1}张图片上传失败:`, data.message)
//
this.uploadImagesSequentially(filePaths, index + 1, uploadedUrls)
}
} catch (error) {
console.error(`${index + 1}张图片解析失败:`, error)
//
this.uploadImagesSequentially(filePaths, index + 1, uploadedUrls)
}
},
fail: (err) => {
console.error(`${index + 1}张图片上传失败:`, err)
//
this.uploadImagesSequentially(filePaths, index + 1, uploadedUrls)
}
})
},

View File

@ -347,8 +347,67 @@ export default {
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// TODO:
this.profile.avatar = res.tempFilePaths[0]
const tempFilePath = res.tempFilePaths[0]
//
uni.showLoading({ title: '上传中...' })
//
uni.uploadFile({
url: `${getApp().globalData.baseUrl}/api/file/upload`,
filePath: tempFilePath,
name: 'file',
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data)
if (data.code === 200) {
// URL使URL
this.profile.avatar = data.data.url || data.data.fileUrl
uni.hideLoading()
uni.showToast({
title: '头像上传成功',
icon: 'success',
duration: 1500
})
} else {
uni.hideLoading()
uni.showToast({
title: data.message || '上传失败',
icon: 'none',
duration: 2000
})
}
} catch (error) {
console.error('解析上传结果失败:', error)
uni.hideLoading()
uni.showToast({
title: '上传失败,请重试',
icon: 'none',
duration: 2000
})
}
},
fail: (err) => {
console.error('上传头像失败:', err)
uni.hideLoading()
uni.showToast({
title: '上传失败,请检查网络',
icon: 'none',
duration: 2000
})
}
})
},
fail: (err) => {
console.log('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none',
duration: 2000
})
}
})
},

View File

@ -8,7 +8,7 @@ import { useUserStore } from '@/store/user'
// 生产环境:使用线上服务器
// 开发模式标志(手动切换)
const IS_DEV = false // 改为 false 则使用生产环境
const IS_DEV = true // 改为 true 则使用本地开发环境
// 根据开发模式选择BASE_URL
const BASE_URL = IS_DEV