Browse Source

修改 bug

master
王泽彦 6 months ago
parent
commit
9554545a9d
  1. 76
      niucloud/app/api/controller/apiController/CourseSchedule.php
  2. 4
      niucloud/app/api/route/route.php
  3. 343
      niucloud/app/service/api/apiService/CourseScheduleService.php
  4. 2
      niucloud/app/service/api/apiService/OrderTableService.php
  5. 4
      uniapp/api/apiRoute.js
  6. 66
      uniapp/components/schedule/ScheduleDetail.vue
  7. 148
      uniapp/pages-coach/coach/schedule/sign_in.vue
  8. 1
      uniapp/pages.json

76
niucloud/app/api/controller/apiController/CourseSchedule.php

@ -307,4 +307,80 @@ class CourseSchedule extends BaseApiService
return fail('获取场地时间选项失败:' . $e->getMessage()); return fail('获取场地时间选项失败:' . $e->getMessage());
} }
} }
/**
* 课程批量签到
* @param Request $request
* @return \think\Response
*/
public function batchSignIn(Request $request)
{
try {
$data = $this->request->params([
["schedule_id", 0],
["students", []],
["remark", ""],
["class_photo", ""]
]);
if (empty($data['schedule_id'])) {
return fail('课程安排ID不能为空');
}
if (empty($data['students'])) {
return fail('学员列表不能为空');
}
$result = (new CourseScheduleService())->batchSignIn($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '签到成功', $result['data'] ?? []);
} catch (\Exception $e) {
return fail('批量签到失败:' . $e->getMessage());
}
}
/**
* 单个学员签到
* @param Request $request
* @return \think\Response
*/
public function updateStudentStatus(Request $request)
{
try {
$data = $this->request->params([
["schedule_id", 0],
["person_id", 0],
["person_type", ""], // student 或 customer_resource
["status", 1], // 签到状态:0-未到,1-已到,2-请假
["reason", ""], // 签到备注
["resources_id", 0] // 资源ID(可选,用于customer_resource类型)
]);
// 验证必填参数
if (empty($data['schedule_id'])) {
return fail('课程安排ID不能为空');
}
if (empty($data['person_id']) && empty($data['resources_id'])) {
return fail('学员ID或资源ID不能为空');
}
// 验证签到状态
if (!in_array($data['status'], [0, 1, 2])) {
return fail('无效的签到状态');
}
$result = (new CourseScheduleService())->updateStudentStatus($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '签到成功', $result['data'] ?? []);
} catch (\Exception $e) {
return fail('签到失败:' . $e->getMessage());
}
}
} }

4
niucloud/app/api/route/route.php

@ -350,6 +350,10 @@ Route::group(function () {
Route::get('courseSchedule/filterOptions', 'apiController.CourseSchedule/getFilterOptions'); Route::get('courseSchedule/filterOptions', 'apiController.CourseSchedule/getFilterOptions');
//员工端-获取场地时间选项 //员工端-获取场地时间选项
Route::get('courseSchedule/venueTimeOptions', 'apiController.CourseSchedule/getVenueTimeOptions'); Route::get('courseSchedule/venueTimeOptions', 'apiController.CourseSchedule/getVenueTimeOptions');
//员工端-课程批量签到
Route::post('courseSchedule/batchSignIn', 'apiController.CourseSchedule/batchSignIn');
//员工端-单个学员签到
Route::post('courseSchedule/updateStudentStatus', 'apiController.CourseSchedule/updateStudentStatus');
// 课程安排统一选项接口(新增-支持校区过滤) // 课程安排统一选项接口(新增-支持校区过滤)
//获取所有排课选项(统一接口-支持校区过滤) //获取所有排课选项(统一接口-支持校区过滤)

343
niucloud/app/service/api/apiService/CourseScheduleService.php

@ -11,6 +11,7 @@
namespace app\service\api\apiService; namespace app\service\api\apiService;
use app\model\student\Student;
use core\base\BaseApiService; use core\base\BaseApiService;
use think\facade\Db; use think\facade\Db;
@ -2434,41 +2435,14 @@ class CourseScheduleService extends BaseApiService
// 开启事务 // 开启事务
Db::startTrans(); Db::startTrans();
// 查找学员记录 // 使用内部处理方法
$enrollment = Db::name('person_course_schedule') $result = $this->processSingleStudentSignIn($scheduleId, $personId, 0, $status, $reason);
->where('schedule_id', $scheduleId)
->where('person_id', $personId)
->where('deleted_at', 0)
->find();
if (!$enrollment) { if (!$result['success']) {
Db::rollback(); Db::rollback();
return [ return [
'code' => 0, 'code' => 0,
'msg' => '找不到学员记录' 'msg' => $result['error']
];
}
// 准备更新数据
$updateData = [
'status' => $status,
'updated_at' => date('Y-m-d H:i:s')
];
if ($reason) {
$updateData['remark'] = $reason;
}
// 更新学员记录
$result = Db::name('person_course_schedule')
->where('id', $enrollment['id'])
->update($updateData);
if ($result === false) {
Db::rollback();
return [
'code' => 0,
'msg' => '更新失败'
]; ];
} }
@ -2558,4 +2532,311 @@ class CourseScheduleService extends BaseApiService
]; ];
} }
} }
/**
* 批量签到
* @param array $data 批量签到数据
* @return array 签到结果
*/
public function batchSignIn(array $data)
{
try {
// 验证必填字段
if (empty($data['schedule_id'])) {
return [
'code' => 0,
'msg' => '课程安排ID不能为空'
];
}
if (empty($data['students']) || !is_array($data['students'])) {
return [
'code' => 0,
'msg' => '学员签到数据不能为空'
];
}
// 开启事务
Db::startTrans();
$scheduleId = $data['schedule_id'];
$students = $data['students'];
$remark = $data['remark'] ?? '';
$classPhoto = $data['class_photo'] ?? '';
// 验证课程安排是否存在
$schedule = Db::name('course_schedule')
->where('id', $scheduleId)
->where('deleted_at', 0)
->find();
if (empty($schedule)) {
Db::rollback();
return [
'code' => 0,
'msg' => '课程安排不存在或已被删除'
];
}
$successCount = 0;
$failedCount = 0;
$errors = [];
// 批量更新学员签到状态 - 优雅复用单个签到逻辑
foreach ($students as $index => $student) {
$studentId = $student['student_id'] ?? 0;
$resourceId = $student['resource_id'] ?? 0;
$status = $student['status'] ?? 1; // 默认为已到
try {
// 验证学员数据
if (empty($studentId) && empty($resourceId)) {
$failedCount++;
$errors[] = "第" . ($index + 1) . "条记录:学员ID或资源ID不能为空";
continue;
}
// 使用内部签到处理方法,避免嵌套事务
$result = $this->processSingleStudentSignIn($scheduleId, $studentId, $resourceId, $status, $remark);
if ($result['success']) {
$successCount++;
} else {
$failedCount++;
$errors[] = "第" . ($index + 1) . "条记录:" . $result['error'];
}
} catch (\Exception $e) {
$failedCount++;
$errors[] = "第" . ($index + 1) . "条记录:处理异常 - " . $e->getMessage();
}
}
// 如果有课堂照片,保存到课程安排记录
if (!empty($classPhoto)) {
// 处理图片上传或保存逻辑
$updateScheduleData = [
'class_photo' => $classPhoto,
'updated_at' => date('Y-m-d H:i:s')
];
// 如果有签到备注,也保存到课程安排
if (!empty($remark)) {
$updateScheduleData['sign_in_remark'] = $remark;
}
Db::name('course_schedule')
->where('id', $scheduleId)
->update($updateScheduleData);
}
// 记录签到历史(可选)
$this->recordSignInHistory($scheduleId, [
'total_students' => count($students),
'success_count' => $successCount,
'failed_count' => $failedCount,
'remark' => $remark,
'class_photo' => $classPhoto,
'sign_in_time' => date('Y-m-d H:i:s'),
'operator_id' => $this->user_id ?? 0
]);
// 检查是否有失败记录
if ($failedCount > 0) {
// 如果有失败记录,但也有成功记录,提示部分成功
if ($successCount > 0) {
Db::commit();
return [
'code' => 1,
'msg' => "批量签到部分成功,成功:{$successCount}人,失败:{$failedCount}人",
'data' => [
'success_count' => $successCount,
'failed_count' => $failedCount,
'errors' => $errors
]
];
} else {
// 全部失败
Db::rollback();
return [
'code' => 0,
'msg' => "批量签到失败,失败:{$failedCount}人",
'data' => [
'success_count' => $successCount,
'failed_count' => $failedCount,
'errors' => $errors
]
];
}
}
// 全部成功
Db::commit();
return [
'code' => 1,
'msg' => "批量签到成功,共签到{$successCount}人",
'data' => [
'success_count' => $successCount,
'failed_count' => $failedCount,
'schedule_id' => $scheduleId
]
];
} catch (\Exception $e) {
Db::rollback();
return [
'code' => 0,
'msg' => '批量签到失败:' . $e->getMessage()
];
}
}
/**
* 记录签到历史
* @param int $scheduleId 课程安排ID
* @param array $signInData 签到数据
* @return void
*/
private function recordSignInHistory($scheduleId, $signInData)
{
try {
// 这里可以记录签到历史到专门的表中
// 如果没有专门的签到历史表,可以记录到日志或其他地方
$historyData = [
'schedule_id' => $scheduleId,
'sign_in_data' => json_encode($signInData),
'created_at' => date('Y-m-d H:i:s')
];
// 假设有一个签到历史表 sign_in_history
// Db::name('sign_in_history')->insert($historyData);
// 或者记录到系统日志
trace('Batch Sign In: ' . json_encode($historyData));
} catch (\Exception $e) {
// 记录日志失败不影响主流程
trace('Record sign in history failed: ' . $e->getMessage());
}
}
/**
* 处理试听课签到逻辑
* @param Student $student 学员模型实例
* @throws \Exception
*/
private function handleTrialClassCheckin($student)
{
try {
$currentTrialCount = $student->trial_class_count;
// 如果试听课次数为0,不进行任何更新
if ($currentTrialCount <= 0) {
return;
}
$updateData = [];
$newTrialCount = $currentTrialCount - 1;
$updateData['trial_class_count'] = max(0, $newTrialCount); // 确保最小值为0
// 根据当前试听次数设置对应的签到时间
if ($currentTrialCount === 2) {
// 第一次试听课签到
$updateData['first_come'] = date('Y-m-d H:i:s');
} elseif ($currentTrialCount === 1) {
// 第二次试听课签到
$updateData['second_come'] = date('Y-m-d H:i:s');
}
// 更新学员试听信息
$result = Student::where('id', $student->id)->update($updateData);
if ($result === false) {
throw new \Exception('更新学员试听信息失败');
}
// 记录日志
trace('Trial class checkin processed', [
'student_id' => $student->id,
'old_trial_count' => $currentTrialCount,
'new_trial_count' => $updateData['trial_class_count'],
'update_fields' => array_keys($updateData)
]);
} catch (\Exception $e) {
// 抛出异常以便外层事务回滚
throw new \Exception('处理试听课签到失败:' . $e->getMessage());
}
}
/**
* 处理单个学员签到(内部方法,不管理事务)
* @param int $scheduleId 课程安排ID
* @param int $studentId 学员ID
* @param int $resourceId 资源ID
* @param int $status 签到状态
* @param string $reason 备注
* @return array 处理结果
*/
private function processSingleStudentSignIn($scheduleId, $studentId, $resourceId, $status, $reason = '')
{
try {
// 查找学员课程安排记录
$whereCondition = [
'schedule_id' => $scheduleId,
'deleted_at' => 0
];
if (!empty($studentId)) {
$whereCondition['student_id'] = $studentId;
} elseif (!empty($resourceId)) {
$whereCondition['resources_id'] = $resourceId;
} else {
return ['success' => false, 'error' => '学员ID或资源ID不能为空'];
}
$enrollment = Db::name('person_course_schedule')
->where($whereCondition)
->find();
if (!$enrollment) {
return ['success' => false, 'error' => '找不到学员记录'];
}
// 获取学员信息(用于试听课处理)
$student = Student::where('id', $enrollment['student_id'])->find();
if (!$student) {
return ['success' => false, 'error' => '找不到学员信息'];
}
// 准备更新数据
$updateData = [
'status' => $status,
'updated_at' => date('Y-m-d H:i:s')
];
if ($reason) {
$updateData['remark'] = $reason;
}
// 更新学员签到记录
$result = Db::name('person_course_schedule')
->where('id', $enrollment['id'])
->update($updateData);
if ($result === false) {
return ['success' => false, 'error' => '更新签到状态失败'];
}
// 处理试听课签到逻辑(仅在签到成功且学员未付费时处理)
if ($status === 1 && $student->pay_status != 1) {
$this->handleTrialClassCheckin($student);
}
return ['success' => true, 'error' => ''];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
} }

2
niucloud/app/service/api/apiService/OrderTableService.php

@ -266,7 +266,7 @@ class OrderTableService extends BaseApiService
return false; return false;
} }
$course = $course->toArray(); $course = $course->toArray();
Student::where('id', $student_id)->update(['status' => 1]); Student::where('id', $student_id)->update(['status' => 1, 'pay_status' => 1]);
// 检查学员是否已有该课程记录 // 检查学员是否已有该课程记录
$existingCourse = Db::table('school_student_courses') $existingCourse = Db::table('school_student_courses')
->where('student_id', $student_id) ->where('student_id', $student_id)

4
uniapp/api/apiRoute.js

@ -1290,7 +1290,7 @@ export default {
// 更新学员课程状态(请假等) // 更新学员课程状态(请假等)
async updateStudentStatus(data = {}) { async updateStudentStatus(data = {}) {
return await http.post('/course/updateStudentStatus', data) return await http.post('/courseSchedule/updateStudentStatus', data)
}, },
@ -1418,7 +1418,7 @@ export default {
// 更新学员状态(新统一接口 - 对接admin端) // 更新学员状态(新统一接口 - 对接admin端)
async updateStudentStatusInArrangement(data = {}) { async updateStudentStatusInArrangement(data = {}) {
try { try {
const response = await http.post('/course/updateStudentStatus', data) const response = await http.post('/courseSchedule/updateStudentStatus', data)
return response return response
} catch (error) { } catch (error) {
console.error('更新学员状态失败:', error) console.error('更新学员状态失败:', error)

66
uniapp/components/schedule/ScheduleDetail.vue

@ -53,9 +53,15 @@
<view class="section students-info"> <view class="section students-info">
<view class="section-header"> <view class="section-header">
<view class="section-title">正式学员 ({{ formalStudents.length }})</view> <view class="section-title">正式学员 ({{ formalStudents.length }})</view>
<view class="arrange-student-btn" @click="handleArrangeStudent"> <view class="header-actions">
<fui-icon name="plus" :size="16" color="#fff"></fui-icon> <view class="action-btn batch-checkin-btn" @click="batchCheckIn">
<text class="btn-text">安排学员</text> <fui-icon name="check" :size="14" color="#fff"></fui-icon>
<text class="btn-text">批量签到</text>
</view>
<view class="action-btn arrange-student-btn" @click="handleArrangeStudent">
<fui-icon name="plus" :size="14" color="#fff"></fui-icon>
<text class="btn-text">安排学员</text>
</view>
</view> </view>
</view> </view>
<view class="cards-grid" v-if="formalStudents && formalStudents.length > 0"> <view class="cards-grid" v-if="formalStudents && formalStudents.length > 0">
@ -731,6 +737,25 @@
return statusTextMap[status] || '未知状态'; return statusTextMap[status] || '未知状态';
}, },
//
batchCheckIn() {
uni.navigateTo({
url: `/pages-coach/coach/schedule/sign_in?id=${this.scheduleId}`,
success: () => {
console.log('跳转到批量签到页面成功');
//
this.closePopup();
},
fail: (error) => {
console.error('跳转到批量签到页面失败:', error);
uni.showToast({
title: '跳转失败,请重试',
icon: 'none'
});
}
});
},
// //
handleArrangeStudent() { handleArrangeStudent() {
try { try {
@ -841,29 +866,44 @@
flex: 1; flex: 1;
} }
.arrange-student-btn { .header-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
background-color: #29d3b4;
border-radius: 6rpx; border-radius: 6rpx;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
&:hover { &:hover {
background-color: #22a68b; opacity: 0.8;
} }
&:active { .btn-text {
background-color: #1e9680; margin-left: 6rpx;
font-size: 24rpx;
color: #fff;
} }
} }
.btn-text { .batch-checkin-btn {
margin-left: 8rpx; background-color: #007bff;
font-size: 24rpx;
color: #fff; &:hover {
font-weight: 500; background-color: #0056b3;
}
}
.arrange-student-btn {
background-color: #29d3b4;
&:hover {
background-color: #22a68b;
}
} }
.waiting-tip { .waiting-tip {

148
uniapp/pages-coach/coach/schedule/sign_in.vue

@ -1,8 +1,5 @@
<template> <template>
<view class="sign-in-container"> <view class="sign-in-container">
<uni-nav-bar title="课程点名" left-icon="left" fixed="true" background-color="#292929" color="#FFFFFF"
@clickLeft="goBack"></uni-nav-bar>
<view class="content"> <view class="content">
<!-- 课程信息 --> <!-- 课程信息 -->
<view class="course-info-card" v-if="scheduleInfo"> <view class="course-info-card" v-if="scheduleInfo">
@ -78,6 +75,24 @@
<fui-textarea v-model="signInRemark" placeholder="请输入点名备注(可选)" maxlength="200"></fui-textarea> <fui-textarea v-model="signInRemark" placeholder="请输入点名备注(可选)" maxlength="200"></fui-textarea>
</view> </view>
<!-- 课堂照片 -->
<view class="photo-section">
<view class="section-title">课堂照片</view>
<view class="photo-upload">
<view v-if="!classPhoto" class="photo-placeholder" @click="chooseImage">
<view class="placeholder-icon">📷</view>
<view class="placeholder-text">点击添加课堂照片</view>
</view>
<view v-else class="photo-preview">
<image :src="classPhoto" mode="aspectFill" class="preview-image" @click="previewImage"></image>
<view class="photo-actions">
<view class="action-btn" @click="chooseImage">重新选择</view>
<view class="action-btn delete" @click="deleteImage">删除</view>
</view>
</view>
</view>
</view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
<view class="submit-btn"> <view class="submit-btn">
<fui-button type="primary" @click="submitSignIn" :loading="submitting">提交点名</fui-button> <fui-button type="primary" @click="submitSignIn" :loading="submitting">提交点名</fui-button>
@ -99,11 +114,14 @@
scheduleInfo: null, scheduleInfo: null,
// //
studentList: [], studentListRaw: [],
// //
signInRemark: '', signInRemark: '',
//
classPhoto: '',
// //
submitting: false submitting: false
}; };
@ -148,10 +166,6 @@
}, },
methods: { methods: {
//
goBack() {
uni.navigateBack();
},
// //
async loadScheduleInfo() { async loadScheduleInfo() {
@ -169,7 +183,7 @@
// //
if (this.scheduleInfo.students && this.scheduleInfo.students.length > 0) { if (this.scheduleInfo.students && this.scheduleInfo.students.length > 0) {
this.studentList = [...this.scheduleInfo.students]; this.studentListRaw = [...this.scheduleInfo.students];
} }
} else { } else {
uni.showToast({ uni.showToast({
@ -255,6 +269,46 @@
}); });
}, },
//
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.classPhoto = res.tempFilePaths[0];
},
fail: (error) => {
console.error('选择图片失败:', error);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
//
previewImage() {
uni.previewImage({
urls: [this.classPhoto],
current: this.classPhoto
});
},
//
deleteImage() {
uni.showModal({
title: '确认删除',
content: '确定要删除这张课堂照片吗?',
success: (res) => {
if (res.confirm) {
this.classPhoto = '';
}
}
});
},
// //
async submitSignIn() { async submitSignIn() {
// //
@ -267,7 +321,8 @@
const submitData = { const submitData = {
schedule_id: this.scheduleId, schedule_id: this.scheduleId,
students: studentData, students: studentData,
remark: this.signInRemark remark: this.signInRemark,
class_photo: this.classPhoto
}; };
this.submitting = true; this.submitting = true;
@ -310,7 +365,6 @@
.sign-in-container { .sign-in-container {
min-height: 100vh; min-height: 100vh;
background-color: #18181c; background-color: #18181c;
padding-top: 88rpx;
} }
.content { .content {
@ -514,6 +568,78 @@
} }
} }
.photo-section {
background-color: #23232a;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 40rpx;
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #fff;
margin-bottom: 20rpx;
}
}
.photo-upload {
width: 100%;
}
.photo-placeholder {
width: 100%;
height: 200rpx;
background-color: #2a2a2a;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2rpx dashed #666;
.placeholder-icon {
font-size: 60rpx;
margin-bottom: 10rpx;
}
.placeholder-text {
font-size: 24rpx;
color: #999;
}
}
.photo-preview {
width: 100%;
.preview-image {
width: 100%;
height: 300rpx;
border-radius: 12rpx;
}
.photo-actions {
display: flex;
gap: 20rpx;
margin-top: 16rpx;
.action-btn {
flex: 1;
height: 60rpx;
background-color: #29d3b4;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #fff;
&.delete {
background-color: #ff3b30;
}
}
}
}
.submit-btn { .submit-btn {
margin-top: 40rpx; margin-top: 40rpx;
padding-bottom: 40rpx; padding-bottom: 40rpx;

1
uniapp/pages.json

@ -546,7 +546,6 @@
"path": "coach/schedule/sign_in", "path": "coach/schedule/sign_in",
"style": { "style": {
"navigationBarTitleText": "课程点名", "navigationBarTitleText": "课程点名",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#292929", "navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }

Loading…
Cancel
Save