diff --git a/admin/src/api/contract.ts b/admin/src/api/contract.ts index 53cd6308..5e7456c1 100644 --- a/admin/src/api/contract.ts +++ b/admin/src/api/contract.ts @@ -61,6 +61,18 @@ export const contractTemplateApi = { savePlaceholderConfig: (contractId: number, data: any) => request.post(`/document_template/config/save`, data), + // 重新识别占位符 + reidentifyPlaceholders: (contractId: number) => + request.post(`/document_template/reidentify/${contractId}`), + + // 更新模板Word文档 + updateTemplateFile: (contractId: number, data: FormData) => + request.post(`/document_template/update_file/${contractId}`, data, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }), + // 保存数据源配置到独立表 saveDataSourceConfig: (contractId: number, data: any) => request.post(`/document_template/config/datasource/save`, { contract_id: contractId, configs: data }), diff --git a/admin/src/app/views/contract/contract.vue b/admin/src/app/views/contract/contract.vue index 9eeb34d7..15fd02fb 100644 --- a/admin/src/app/views/contract/contract.vue +++ b/admin/src/app/views/contract/contract.vue @@ -159,7 +159,65 @@
-

检测到的占位符 (合同ID: {{ currentContractId }})

+
+

检测到的占位符 (合同ID: {{ currentContractId }})

+
+ + + + + + +
+
+ + +
+
重新上传Word文档
+
+
+ +
+ +
支持 .docx 和 .doc 格式文件,文件大小不超过 10MB
+
+ 📄 {{ reuploadFileName }} + +
+
+
+
+ + +
+
+
@@ -412,8 +470,17 @@ const currentContract = ref(null) const uploading = ref(false) const configLoading = ref(false) const configList = ref([]) +const reidentifying = ref(false) const fileInputKey = ref(0) const fileInput = ref() + +// 重新上传Word文档相关状态 +const showReuploadSection = ref(false) +const reuploadFile = ref(null) +const reuploadFileName = ref('') +const reuploading = ref(false) +const reuploadFileInput = ref() +const currentTemplateInfo = ref(null) const staffList = ref([]) const filteredStaffList = ref([]) const staffLoading = ref(false) @@ -519,7 +586,10 @@ const updateStatus = async (row: ContractTemplate) => { const configPlaceholder = async (row: ContractTemplate) => { currentContractId.value = row.id + currentTemplateInfo.value = row // 保存当前模板信息 showConfigDialog.value = true + showReuploadSection.value = false // 重置重新上传区域 + clearReuploadFile() // 清空重新上传的文件 // 加载占位符配置数据 await loadPlaceholderConfig(row.id) } @@ -665,12 +735,66 @@ const handleUploadSuccess = () => { getList() } +// 重新识别占位符 +const reidentifyPlaceholders = async () => { + if (!currentContractId.value) { + ElMessage.error('未找到合同ID') + return + } + + reidentifying.value = true + try { + console.log('🔍 开始重新识别占位符, 合同ID:', currentContractId.value) + + // 调用重新识别接口 + const { data } = await contractTemplateApi.reidentifyPlaceholders(currentContractId.value) + console.log('✅ 重新识别成功, 返回数据:', data) + + // 展示详细的成功信息 + const { placeholders, placeholder_count, new_placeholders, removed_placeholders } = data + let message = `成功识别到 ${placeholder_count} 个占位符` + + if (new_placeholders && new_placeholders.length > 0) { + message += `,新增 ${new_placeholders.length} 个` + } + if (removed_placeholders && removed_placeholders.length > 0) { + message += `,移除 ${removed_placeholders.length} 个` + } + + ElMessage.success(message) + + // 重新加载配置数据 + await loadPlaceholderConfig(currentContractId.value) + + } catch (error) { + console.error('❌ 重新识别失败:', error) + + // 根据错误类型显示不同的提示 + let errorMessage = error.message || '未知错误' + + if (errorMessage.includes('模板未上传Word文档')) { + ElMessage.error('请先上传Word文档模板后再进行占位符识别') + } else if (errorMessage.includes('模板文件不存在')) { + ElMessage.error('模板文件不存在,请重新上传') + } else { + ElMessage.error(`重新识别失败: ${errorMessage}`) + } + } finally { + reidentifying.value = false + } +} + // 加载占位符配置 const loadPlaceholderConfig = async (contractId: number) => { configLoading.value = true try { const { data } = await contractTemplateApi.getPlaceholderConfig(contractId) console.log('API返回数据:', data) + + // 更新当前模板信息 + if (data && typeof data === 'object') { + currentTemplateInfo.value = data + } // 处理API返回的数据格式,支持新的三种数据类型 if (data && typeof data === 'object') { @@ -995,6 +1119,100 @@ const clearFile = () => { // fileInputKey.value += 1 // 注释掉,避免意外重新渲染导致文件丢失 } +// 重新上传文件选择处理 +const handleReuploadFileSelect = (event: Event) => { + const target = event.target as HTMLInputElement + const file = target.files?.[0] + + console.log('📁 重新上传文件选择:', file) + + if (!file) { + reuploadFile.value = null + reuploadFileName.value = '' + return + } + + // 检查文件类型 + const fileName = file.name.toLowerCase() + const allowedExtensions = ['.docx', '.doc'] + const isValidType = allowedExtensions.some(ext => fileName.endsWith(ext)) + + if (!isValidType) { + ElMessage.error('只支持上传 .docx 和 .doc 格式的文件!') + clearReuploadFile() + return + } + + // 检查文件大小 (10MB) + if (file.size > 10 * 1024 * 1024) { + ElMessage.error('文件大小不能超过 10MB!') + clearReuploadFile() + return + } + + // 存储文件信息 + reuploadFile.value = file + reuploadFileName.value = file.name + + console.log('✅ 重新上传文件选择成功:', { name: file.name, size: file.size }) +} + +// 清空重新上传文件 +const clearReuploadFile = () => { + reuploadFile.value = null + reuploadFileName.value = '' + if (reuploadFileInput.value) { + reuploadFileInput.value.value = '' + } +} + +// 提交重新上传 +const submitReupload = async () => { + if (!reuploadFile.value) { + ElMessage.error('请选择Word文档') + return + } + + if (!currentContractId.value) { + ElMessage.error('未找到模板ID') + return + } + + reuploading.value = true + try { + console.log('🚀 开始重新上传Word文档...', { + templateId: currentContractId.value, + fileName: reuploadFile.value.name, + fileSize: reuploadFile.value.size + }) + + // 构建FormData + const formData = new FormData() + formData.append('file', reuploadFile.value) + + // 调用更新模板文档的API + await contractTemplateApi.updateTemplateFile(currentContractId.value, formData) + + console.log('✅ 重新上传成功') + ElMessage.success('Word文档更新成功') + + // 重置状态 + showReuploadSection.value = false + clearReuploadFile() + + // 重新加载模板信息和配置 + await loadPlaceholderConfig(currentContractId.value) + // 更新列表中的模板信息 + getList() + + } catch (error) { + console.error('❌ 重新上传失败:', error) + ElMessage.error(`更新文档失败: ${error.message || '未知错误'}`) + } finally { + reuploading.value = false + } +} + // 提交上传 const submitUpload = async () => { console.log('🚀 开始上传流程...') @@ -1187,6 +1405,114 @@ onMounted(() => { margin-bottom: 15px; } +.config-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.config-header h4 { + margin: 0; + color: #303133; +} + +.header-buttons { + display: flex; + gap: 8px; + align-items: center; +} + +.btn-reidentify { + padding: 6px 12px; + border: 1px solid #67c23a; + background: #67c23a; + color: white; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s; +} + +.btn-reidentify:hover:not(:disabled) { + background: #85ce61; + border-color: #85ce61; +} + +.btn-reidentify:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-upload { + padding: 6px 12px; + border: 1px solid #409eff; + background: #409eff; + color: white; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s; +} + +.btn-upload:hover:not(:disabled) { + background: #66b1ff; + border-color: #66b1ff; +} + +.btn-upload:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-secondary { + padding: 6px 12px; + border: 1px solid #909399; + background: #909399; + color: white; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s; +} + +.btn-secondary:hover:not(:disabled) { + background: #a6a9ad; + border-color: #a6a9ad; +} + +.btn-secondary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.reupload-section { + margin: 20px 0; + padding: 15px; + background: #f9f9f9; + border: 1px solid #e4e7ed; + border-radius: 4px; +} + +.reupload-section h5 { + margin: 0 0 15px 0; + color: #303133; + font-size: 14px; + font-weight: 500; +} + +.reupload-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.reupload-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + .config-table { width: 100%; border-collapse: collapse; diff --git a/niucloud/app/adminapi/controller/document/DocumentTemplate.php b/niucloud/app/adminapi/controller/document/DocumentTemplate.php index fa61cacd..fa33c0dd 100644 --- a/niucloud/app/adminapi/controller/document/DocumentTemplate.php +++ b/niucloud/app/adminapi/controller/document/DocumentTemplate.php @@ -309,4 +309,40 @@ class DocumentTemplate extends BaseAdminController return fail($e->getMessage()); } } + + /** + * 重新识别占位符 + * @param int $id + * @return \think\Response + */ + public function reidentify(int $id) + { + try { + $result = (new DocumentTemplateService())->reidentifyPlaceholders($id); + return success('重新识别成功', $result); + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 更新模板Word文档 + * @param int $id + * @return \think\Response + */ + public function updateFile(int $id) + { + try { + // 获取上传的文件 + $file = Request::file('file'); + if (!$file) { + return fail('请选择要上传的Word文档'); + } + + $result = (new DocumentTemplateService())->updateTemplateFile($id, $file); + return success('文档更新成功', $result); + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } } \ No newline at end of file diff --git a/niucloud/app/adminapi/route/document_template.php b/niucloud/app/adminapi/route/document_template.php index 131d7f10..75a3ba73 100644 --- a/niucloud/app/adminapi/route/document_template.php +++ b/niucloud/app/adminapi/route/document_template.php @@ -34,6 +34,8 @@ Route::group('document_template', function () { Route::post('config/save', 'document.DocumentTemplate/savePlaceholderConfig'); Route::post('config/datasource/save', 'document.DocumentTemplate/saveDataSourceConfig'); Route::get('datasources', 'document.DocumentTemplate/getDataSources'); + Route::post('reidentify/:id', 'document.DocumentTemplate/reidentify'); + Route::post('update_file/:id', 'document.DocumentTemplate/updateFile'); // 文档生成 Route::post('generate', 'document.DocumentTemplate/generateDocument'); diff --git a/niucloud/app/api/controller/apiController/Course.php b/niucloud/app/api/controller/apiController/Course.php index 68fe4504..eaa0a2d3 100644 --- a/niucloud/app/api/controller/apiController/Course.php +++ b/niucloud/app/api/controller/apiController/Course.php @@ -104,6 +104,7 @@ class Course extends BaseApiService public function addSchedule(Request $request){ $data = $this->request->params([ ["resources_id",''], + ["student_id",''], ["person_type",''], ["schedule_id",''], ["course_date",''], diff --git a/niucloud/app/api/controller/apiController/PersonCourseSchedule.php b/niucloud/app/api/controller/apiController/PersonCourseSchedule.php index a402f265..01203298 100644 --- a/niucloud/app/api/controller/apiController/PersonCourseSchedule.php +++ b/niucloud/app/api/controller/apiController/PersonCourseSchedule.php @@ -160,6 +160,7 @@ class PersonCourseSchedule extends BaseApiService public function getStudentCourseInfo(Request $request){ $resource_id = $request->param('resource_id', '');//客户资源ID $member_id = $request->param('member_id', '');//会员ID + $student_id = $request->param('student_id', '');//学生ID if (empty($resource_id)) { return fail('缺少参数resource_id'); @@ -168,6 +169,7 @@ class PersonCourseSchedule extends BaseApiService $where = [ 'resource_id' => $resource_id, 'member_id' => $member_id, + 'student_id' => $student_id, ]; $res = (new PersonCourseScheduleService())->getStudentCourseInfo($where); diff --git a/niucloud/app/api/route/student.php b/niucloud/app/api/route/student.php index af0b2315..fe4c6622 100644 --- a/niucloud/app/api/route/student.php +++ b/niucloud/app/api/route/student.php @@ -99,7 +99,7 @@ Route::group('contract', function () { Route::get('download/:contract_id', 'app\api\controller\student\ContractController@downloadContract'); // 获取学员基本信息 Route::get('student-info', 'app\api\controller\student\ContractController@getStudentInfo'); -}); +})->middleware(['ApiCheckToken']); // 知识库(测试版本,无需token) Route::group('knowledge-test', function () { diff --git a/niucloud/app/model/document/DocumentDataSourceConfig.php b/niucloud/app/model/document/DocumentDataSourceConfig.php index 2295ef0f..2ee049e7 100644 --- a/niucloud/app/model/document/DocumentDataSourceConfig.php +++ b/niucloud/app/model/document/DocumentDataSourceConfig.php @@ -11,7 +11,7 @@ use app\model\contract\Contract; class DocumentDataSourceConfig extends BaseModel { protected $pk = 'id'; - protected $name = 'school_document_data_source_config'; + protected $name = 'document_data_source_config'; /** * 关联合同表 diff --git a/niucloud/app/service/admin/course_schedule/CourseScheduleService.php b/niucloud/app/service/admin/course_schedule/CourseScheduleService.php index ec212ec9..848652c5 100644 --- a/niucloud/app/service/admin/course_schedule/CourseScheduleService.php +++ b/niucloud/app/service/admin/course_schedule/CourseScheduleService.php @@ -328,8 +328,10 @@ class CourseScheduleService extends BaseAdminService public function addSchedule(array $data){ $CourseSchedule = new CourseSchedule(); $personCourseSchedule = new PersonCourseSchedule(); + dd($data); if($personCourseSchedule->where([ 'resources_id' => $data['resources_id'], + 'student_id' => $data['student_id'], 'schedule_id' => $data['schedule_id'] ])->find()){ return fail("重复添加"); @@ -337,6 +339,7 @@ class CourseScheduleService extends BaseAdminService $personCourseSchedule->insert([ 'resources_id' => $data['resources_id'], + 'student_id' => $data['student_id'], 'person_id' => 1, 'person_type' => $data['person_type'], 'schedule_id' => $data['schedule_id'], diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php index 5a70de3e..b71cc546 100644 --- a/niucloud/app/service/admin/document/DocumentTemplateService.php +++ b/niucloud/app/service/admin/document/DocumentTemplateService.php @@ -454,7 +454,6 @@ class DocumentTemplateService extends BaseAdminService 'table_name' => $settings['table_name'] ?? '', 'field_name' => $settings['field_name'] ?? '', 'system_function' => $settings['system_function'] ?? '', - 'user_input_value' => $settings['user_input_value'] ?? '', 'field_type' => $settings['field_type'] ?? 'text', 'is_required' => $settings['is_required'] ?? 0, 'default_value' => $settings['default_value'] ?? '', @@ -961,4 +960,223 @@ class DocumentTemplateService extends BaseAdminService return $template->save(); } + + /** + * 重新识别占位符 + * @param int $id 模板ID + * @return array + * @throws \Exception + */ + public function reidentifyPlaceholders(int $id) + { + $template = $this->contractModel->find($id); + if (!$template) { + throw new \Exception('模板不存在'); + } + + // 检查模板文件路径是否存在 + if (empty($template['contract_template'])) { + throw new \Exception('模板未上传Word文档,无法识别占位符'); + } + + // 检查模板文件是否存在 + $templatePath = public_path() . '/upload/' . $template['contract_template']; + if (!file_exists($templatePath)) { + throw new \Exception('模板文件不存在:' . $template['contract_template']); + } + + // 检查是否为文件而不是目录 + if (is_dir($templatePath)) { + throw new \Exception('模板路径指向的是目录而不是文件:' . $template['contract_template']); + } + + try { + // 重新解析Word文档内容和占位符 + $parseResult = $this->parseWordTemplate($templatePath); + + // 更新数据库中的占位符列表 + $template->placeholders = json_encode($parseResult['placeholders']); + $template->contract_content = $parseResult['content']; + $template->updated_at = date('Y-m-d H:i:s'); + $template->save(); + + // 获取现有的占位符配置 + $existingConfig = []; + if ($template['placeholder_config']) { + $existingConfig = json_decode($template['placeholder_config'], true) ?: []; + } + + // 为新的占位符创建默认配置,保留现有配置 + $newConfig = []; + foreach ($parseResult['placeholders'] as $placeholder) { + if (isset($existingConfig[$placeholder])) { + // 保留现有配置 + $newConfig[$placeholder] = $existingConfig[$placeholder]; + } else { + // 为新占位符创建默认配置 + $newConfig[$placeholder] = [ + 'data_type' => 'user_input', + 'table_name' => '', + 'field_name' => '', + 'system_function' => '', + 'user_input_value' => '', + 'field_type' => 'text', + 'is_required' => 0, + 'default_value' => '' + ]; + } + } + + // 更新占位符配置 + if (!empty($newConfig)) { + $template->placeholder_config = json_encode($newConfig); + $template->save(); + + // 同步更新数据源配置表 + $this->saveConfigToDataSourceTable($id, $newConfig); + } + + return [ + 'placeholders' => $parseResult['placeholders'], + 'placeholder_count' => count($parseResult['placeholders']), + 'new_placeholders' => array_diff($parseResult['placeholders'], array_keys($existingConfig)), + 'removed_placeholders' => array_diff(array_keys($existingConfig), $parseResult['placeholders']) + ]; + + } catch (\Exception $e) { + Log::error('重新识别占位符失败:' . $e->getMessage()); + throw new \Exception('重新识别占位符失败:' . $e->getMessage()); + } + } + + /** + * 更新模板Word文档 + * @param int $id 模板ID + * @param $file 上传的文件 + * @return array + * @throws \Exception + */ + public function updateTemplateFile(int $id, $file) + { + $template = $this->contractModel->find($id); + if (!$template) { + throw new \Exception('模板不存在'); + } + + // 验证文件类型 + $allowedTypes = ['docx', 'doc']; + $extension = strtolower($file->getOriginalExtension()); + + if (!in_array($extension, $allowedTypes)) { + throw new \Exception('只支持 .docx 和 .doc 格式的Word文档'); + } + + // 获取文件信息 + $fileSize = $file->getSize(); + $realPath = $file->getRealPath(); + + // 验证文件大小 (ORM大10MB) + $maxSize = 10 * 1024 * 1024; + if ($fileSize > $maxSize) { + throw new \Exception('文件大小不能超过10MB'); + } + + // 生成文件hash防重复 + $fileHash = md5_file($realPath); + + try { + // 删除旧文件 + if ($template['contract_template']) { + $oldFilePath = public_path() . '/upload/' . $template['contract_template']; + if (file_exists($oldFilePath) && is_file($oldFilePath)) { + unlink($oldFilePath); + } + } + + // 生成保存路径 + $uploadDir = 'contract_templates/' . date('Ymd'); + $uploadPath = public_path() . '/upload/' . $uploadDir; + + // 确保目录存在 + if (!is_dir($uploadPath)) { + mkdir($uploadPath, 0777, true); + } + + // 生成文件名 + $fileName = md5(time() . $file->getOriginalName()) . '.' . $extension; + $fullPath = $uploadPath . '/' . $fileName; + $savePath = $uploadDir . '/' . $fileName; + + // 移动文件到目标位置 + if (!move_uploaded_file($realPath, $fullPath)) { + throw new \Exception('文件保存失败'); + } + + // 解析Word文档内容和占位符 + $parseResult = $this->parseWordTemplate($fullPath); + + // 更新数据库记录 + $template->contract_template = $savePath; + $template->contract_content = $parseResult['content']; + $template->original_filename = $file->getOriginalName(); + $template->file_size = $fileSize; + $template->file_hash = $fileHash; + $template->placeholders = json_encode($parseResult['placeholders']); + $template->updated_at = date('Y-m-d H:i:s'); + $template->save(); + + // 获取现有的占位符配置 + $existingConfig = []; + if ($template['placeholder_config']) { + $existingConfig = json_decode($template['placeholder_config'], true) ?: []; + } + + // 为新的占位符创建默认配置,保留现有配置 + $newConfig = []; + foreach ($parseResult['placeholders'] as $placeholder) { + if (isset($existingConfig[$placeholder])) { + // 保留现有配置 + $newConfig[$placeholder] = $existingConfig[$placeholder]; + } else { + // 为新占位符创建默认配置 + $newConfig[$placeholder] = [ + 'data_type' => 'user_input', + 'table_name' => '', + 'field_name' => '', + 'system_function' => '', + 'user_input_value' => '', + 'field_type' => 'text', + 'is_required' => 0, + 'default_value' => '' + ]; + } + } + + // 更新占位符配置 + if (!empty($newConfig)) { + $template->placeholder_config = json_encode($newConfig); + $template->save(); + + // 同步更新数据源配置表 + $this->saveConfigToDataSourceTable($id, $newConfig); + } + + return [ + 'id' => $template->id, + 'template_name' => $template->contract_name, + 'file_path' => $savePath, + 'placeholders' => $parseResult['placeholders'], + 'placeholder_count' => count($parseResult['placeholders']), + 'new_placeholders' => array_diff($parseResult['placeholders'], array_keys($existingConfig)), + 'removed_placeholders' => array_diff(array_keys($existingConfig), $parseResult['placeholders']) + ]; + + } catch (\Exception $e) { + // 如果保存失败,删除已上传的文件 + if (isset($fullPath) && file_exists($fullPath)) { + unlink($fullPath); + } + throw new \Exception('模板文档更新失败:' . $e->getMessage()); + } + } } \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/CourseService.php b/niucloud/app/service/api/apiService/CourseService.php index e017ed0f..0792edba 100644 --- a/niucloud/app/service/api/apiService/CourseService.php +++ b/niucloud/app/service/api/apiService/CourseService.php @@ -431,82 +431,55 @@ class CourseService extends BaseApiService public function addSchedule(array $data){ $CourseSchedule = new CourseSchedule(); $personCourseSchedule = new PersonCourseSchedule(); - - // 根据person_type确定正确的student_id和resources_id - $student_id = 0; - $resources_id = 0; - - if ($data['person_type'] == 'student') { - // 如果是学员类型,从传入的数据中获取student_id,然后查询对应的resources_id - $student_id = $data['resources_id']; // 前端传来的是student.id - - // 通过student表查询对应的user_id(即customer_resources.id) - $Student = new Student(); - $student = $Student->where('id', $student_id)->find(); - if (!$student) { - return fail("学员不存在"); - } - $resources_id = $student['user_id']; // student.user_id = customer_resources.id - - } else if ($data['person_type'] == 'customer_resource') { - // 如果是客户资源类型,直接使用传入的resources_id - $resources_id = $data['resources_id']; - - // 验证客户资源是否存在 - $customerResource = Db::name('customer_resources') - ->where('id', $resources_id) - ->find(); - - if (!$customerResource) { - return fail("客户资源不存在"); - } - - // 通过customer_resources.id查找对应的学生记录 - // school_student.user_id = school_customer_resources.id - $Student = new Student(); - $student = $Student->where('user_id', $resources_id)->find(); - - if (!$student) { - return fail("该客户资源没有关联的学生记录"); - } - - $student_id = $student['id']; - } else { - return fail("无效的人员类型"); + $student = Student::where('id', $data['student_id'])->find(); + if (!$student) { + return fail("学员不存在"); } - - // 检查重复添加 - 根据person_type使用不同的检查逻辑 - $checkWhere = ['schedule_id' => $data['schedule_id']]; - if ($data['person_type'] == 'student') { - $checkWhere['student_id'] = $student_id; - } else { - $checkWhere['resources_id'] = $resources_id; + + $studentCourse = StudentCourses::where('student_id', $student->id) + ->order('id', 'desc') + ->find(); + if ($studentCourse){ + $person_type = 'student'; + }else{ + $person_type = 'customer_resource'; } - - if($personCourseSchedule->where($checkWhere)->find()){ - return fail("重复添加"); + + // 检查重复添加 - 根据person_type使用不同的检查逻辑 + $checkWhere = [ + 'schedule_id' => $data['schedule_id'], + 'student_id'=>$student->id, + 'resources_id'=>$data['resources_id'], + ]; + $course = $personCourseSchedule->where($checkWhere)->find(); + if($course){ + if ($course->schedule_type == 2 && $course->course_type == 1) { + $course->schedule_type == 1; + $course->save(); + }else{ + return fail("重复添加"); + } } - // 调试:插入前记录变量值 - error_log("Debug: Before insert - student_id = " . $student_id . ", resources_id = " . $resources_id); - $insertData = [ - 'student_id' => $student_id, // 正确设置student_id - 'resources_id' => $resources_id, // 正确设置resources_id + 'student_id' => $student->id, // 正确设置student_id + 'resources_id' => $data['resources_id'], // 正确设置resources_id 'person_id' => $this->member_id, - 'person_type' => $data['person_type'], + 'person_type' => $person_type, 'schedule_id' => $data['schedule_id'], 'course_date' => $data['course_date'], 'time_slot' => $data['time_slot'], 'schedule_type' => $data['schedule_type'] ?? 1, // 1=正式位, 2=等待位 - 'course_type' => $data['course_type'] ?? 1, // 1=正式课, 2=体验课, 3=等待位 + 'course_type' => empty($course) ? 2 : 1, // 1=正式学员, 2=体验课学员 'remark' => $data['remark'] ?? '' // 备注 ]; - - error_log("Debug: Insert data = " . json_encode($insertData)); - + $personCourseSchedule->insert($insertData); - $CourseSchedule->where(['id' => $data['schedule_id']])->dec("available_capacity")->update(); + $student_ids = $personCourseSchedule->where(['schedule_id' => $data['schedule_id']])->column('student_id'); + $CourseSchedule->where(['id' => $data['schedule_id']])->update([ + 'student_ids'=>$student_ids, + 'available_capacity'=>count($student_ids) + ]); return success("添加成功"); } @@ -543,7 +516,8 @@ class CourseService extends BaseApiService // 查询记录 $record = $personCourseSchedule->where([ 'schedule_id' => $data['id'], - 'resources_id' => $data['resources_id'] + 'resources_id' => $data['resources_id'], + ])->find(); if (!$record) { diff --git a/niucloud/app/service/api/apiService/PersonCourseScheduleService.php b/niucloud/app/service/api/apiService/PersonCourseScheduleService.php index 0e9693cc..e84917d5 100644 --- a/niucloud/app/service/api/apiService/PersonCourseScheduleService.php +++ b/niucloud/app/service/api/apiService/PersonCourseScheduleService.php @@ -366,9 +366,15 @@ class PersonCourseScheduleService extends BaseApiService 'data' => [] ]; - // 直接通过resource_id查询学员课程表 - $studentCourses = StudentCourses::where('resource_id', $where['resource_id']) - ->with([ + // 构建查询条件 + $query = StudentCourses::where('resource_id', $where['resource_id']); + + // 如果传入了student_id参数,添加student_id条件 + if (!empty($where['student_id'])) { + $query = $query->where('student_id', $where['student_id']); + } + + $studentCourses = $query->with([ 'course' => function($query) { $query->field('id,course_name'); }, diff --git a/niucloud/app/service/api/apiService/StudentService.php b/niucloud/app/service/api/apiService/StudentService.php index c0d981d7..2b54ed61 100644 --- a/niucloud/app/service/api/apiService/StudentService.php +++ b/niucloud/app/service/api/apiService/StudentService.php @@ -95,18 +95,8 @@ class StudentService extends BaseApiService try { // 获取当前登录人员的ID $currentUserId = $this->getUserId(); - - if (empty($currentUserId)) { - $res['code'] = 1; - $res['data'] = []; - $res['msg'] = '获取成功'; - return $res; - } - // 查询符合条件的学生ID集合 - $studentIds = $this->getStudentIds($currentUserId, $data); - - if (empty($studentIds)) { + if (empty($currentUserId)) { $res['code'] = 1; $res['data'] = []; $res['msg'] = '获取成功'; @@ -116,9 +106,12 @@ class StudentService extends BaseApiService // 构建学员基础查询条件 $where = []; $where[] = ['s.deleted_at', '=', 0]; - $where[] = ['s.id', 'in', $studentIds]; // 支持搜索参数 + if (!empty($data['parent_resource_id'])){ + $where[] = ['s.user_id', '=', $data['parent_resource_id']]; + } + if (!empty($data['name'])) { $where[] = ['s.name', 'like', '%' . $data['name'] . '%']; } diff --git a/uniapp/App.vue b/uniapp/App.vue index 27a1b37b..a9eb0f14 100644 --- a/uniapp/App.vue +++ b/uniapp/App.vue @@ -8,6 +8,9 @@ var wxtoken = new WxToken(); export default { async onLaunch() { + // 检查小程序更新 + this.checkForUpdate(); + // #ifdef MP-WEIXIN uni.login({ provider: 'weixin', @@ -36,8 +39,206 @@ //微信公众号获取token -必须是认证的服务号 // #endif }, - onShow: function() {}, + onShow: function() { + // 应用切换到前台时再次检查更新 + this.checkForUpdate(); + }, onHide: function() {}, + + methods: { + /** + * 检查小程序更新 + */ + checkForUpdate() { + // #ifdef MP-WEIXIN + try { + const updateManager = uni.getUpdateManager(); + + // 监听向微信后台请求检查更新结果事件 + updateManager.onCheckForUpdate((res) => { + console.log('检查更新结果:', res); + if (res.hasUpdate) { + console.log('发现新版本,准备下载'); + uni.showToast({ + title: '发现新版本', + icon: 'none', + duration: 2000 + }); + } + }); + + // 监听小程序有版本更新事件 + updateManager.onUpdateReady(() => { + console.log('新版本下载完成,准备重启应用'); + this.showUpdateDialog(); + }); + + // 监听小程序更新失败事件 + updateManager.onUpdateFailed(() => { + console.log('新版本下载失败'); + uni.showToast({ + title: '更新失败,请检查网络', + icon: 'none', + duration: 2000 + }); + }); + + // 主动检查更新 + updateManager.checkForUpdate(); + } catch (error) { + console.error('更新管理器初始化失败:', error); + } + // #endif + + // #ifdef H5 + // H5环境下的更新检查(可选) + this.checkH5Update(); + // #endif + }, + + /** + * 显示更新对话框 + */ + showUpdateDialog() { + uni.showModal({ + title: '更新提示', + content: '新版本已下载完成,是否立即重启应用以更新到最新版本?', + confirmText: '立即重启', + cancelText: '稍后重启', + success: (res) => { + if (res.confirm) { + console.log('用户选择立即重启'); + this.applyUpdate(); + } else { + console.log('用户选择稍后重启'); + // 可以设置定时器在一定时间后自动重启 + this.scheduleDelayedUpdate(); + } + } + }); + }, + + /** + * 应用更新并重启 + */ + applyUpdate() { + // #ifdef MP-WEIXIN + try { + const updateManager = uni.getUpdateManager(); + updateManager.applyUpdate(); + } catch (error) { + console.error('应用更新失败:', error); + uni.showToast({ + title: '重启失败,请手动重启', + icon: 'none', + duration: 2000 + }); + } + // #endif + }, + + /** + * 安排延迟更新 + */ + scheduleDelayedUpdate() { + // 5分钟后自动重启 + setTimeout(() => { + console.log('自动应用更新'); + this.applyUpdate(); + }, 5 * 60 * 1000); + + uni.showToast({ + title: '将在5分钟后自动重启', + icon: 'none', + duration: 3000 + }); + }, + + /** + * H5环境下的更新检查(可选功能) + */ + checkH5Update() { + // #ifdef H5 + try { + // 检查页面缓存版本 + const currentVersion = uni.getStorageSync('app_version') || '1.0.0'; + const buildTime = uni.getStorageSync('build_time') || Date.now(); + const now = Date.now(); + + // 每小时检查一次更新 + if (now - buildTime > 60 * 60 * 1000) { + console.log('H5环境检查更新'); + // 这里可以调用API检查是否有新版本 + // 如果有新版本,可以提示用户刷新页面 + this.checkVersionFromServer(); + } + } catch (error) { + console.error('H5更新检查失败:', error); + } + // #endif + }, + + /** + * 从服务器检查版本信息(H5环境) + */ + async checkVersionFromServer() { + try { + // 这里应该调用你的API来获取最新版本信息 + // const response = await uni.request({ + // url: Api_url.domain + '/api/app/version', + // method: 'GET' + // }); + // + // if (response.data.code === 1) { + // const serverVersion = response.data.data.version; + // const currentVersion = uni.getStorageSync('app_version') || '1.0.0'; + // + // if (this.compareVersion(serverVersion, currentVersion) > 0) { + // this.showH5UpdateDialog(); + // } + // } + + console.log('从服务器检查H5版本更新'); + } catch (error) { + console.error('服务器版本检查失败:', error); + } + }, + + /** + * 显示H5更新对话框 + */ + showH5UpdateDialog() { + uni.showModal({ + title: '发现新版本', + content: '检测到新版本,建议刷新页面以获得最佳体验', + confirmText: '立即刷新', + cancelText: '稍后刷新', + success: (res) => { + if (res.confirm) { + location.reload(); + } + } + }); + }, + + /** + * 版本号比较工具 + */ + compareVersion(version1, version2) { + const v1 = version1.split('.').map(Number); + const v2 = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1.length, v2.length); i++) { + const a = v1[i] || 0; + const b = v2[i] || 0; + + if (a > b) return 1; + if (a < b) return -1; + } + + return 0; + } + } } diff --git a/uniapp/components/fitness-record-popup/fitness-record-popup.vue b/uniapp/components/fitness-record-popup/fitness-record-popup.vue index ea4049e8..492807c7 100644 --- a/uniapp/components/fitness-record-popup/fitness-record-popup.vue +++ b/uniapp/components/fitness-record-popup/fitness-record-popup.vue @@ -227,80 +227,147 @@ export default { // 选择PDF文件 selectPDFFiles() { - uni.chooseFile({ + // 动态检测并选择合适的文件选择API + let chooseFileMethod = null; + + // #ifdef MP-WEIXIN + // 微信小程序优先使用 chooseMessageFile + if (typeof uni.chooseMessageFile === 'function') { + chooseFileMethod = uni.chooseMessageFile; + } else if (typeof wx !== 'undefined' && typeof wx.chooseMessageFile === 'function') { + chooseFileMethod = wx.chooseMessageFile; + } + // #endif + + // #ifndef MP-WEIXIN + // 其他平台使用 chooseFile + if (typeof uni.chooseFile === 'function') { + chooseFileMethod = uni.chooseFile; + } + // #endif + + // 如果没有找到合适的方法,显示错误提示 + if (!chooseFileMethod) { + uni.showModal({ + title: '不支持文件选择', + content: '当前平台不支持文件选择功能,请手动输入文件信息或联系技术支持', + showCancel: false + }); + return; + } + + // 调用选择的文件选择方法 + chooseFileMethod({ count: 5, type: 'file', extension: ['pdf'], success: async (res) => { console.log('选择的文件:', res.tempFiles) + await this.handleSelectedFiles(res.tempFiles) + }, + fail: (err) => { + console.error('选择文件失败:', err) + // 提供更详细的错误信息 + let errorMsg = '选择文件失败'; + if (err.errMsg) { + if (err.errMsg.includes('cancel')) { + errorMsg = '用户取消了文件选择'; + } else if (err.errMsg.includes('limit')) { + errorMsg = '文件数量或大小超出限制'; + } else { + errorMsg = '选择文件失败: ' + err.errMsg; + } + } - // 显示上传进度 - uni.showLoading({ - title: '上传中...', - mask: true + uni.showToast({ + title: errorMsg, + icon: 'none', + duration: 3000 }) - - let successCount = 0 - let totalCount = res.tempFiles.length - - for (let file of res.tempFiles) { - if (file.type === 'application/pdf') { - try { - // 立即上传PDF文件到服务器(使用通用文档上传接口) - const uploadResult = await this.uploadPdfFile(file) - if (uploadResult && uploadResult.code === 1) { - const pdfFile = { - id: Date.now() + Math.random(), - name: file.name, - size: file.size, - url: uploadResult.data.url, // 使用标准响应中的url字段 - server_path: uploadResult.data.url, // 服务器可访问路径 - upload_time: new Date().toLocaleString(), - ext: uploadResult.data.ext || 'pdf', - original_name: uploadResult.data.name || file.name - } - this.recordData.pdf_files.push(pdfFile) - successCount++ - } else { - console.error('文件上传失败:', uploadResult) - uni.showToast({ - title: uploadResult.msg || '文件上传失败', - icon: 'none' - }) - } - } catch (error) { - console.error('上传PDF文件失败:', error) - uni.showToast({ - title: '文件上传失败: ' + (error.msg || error.message || '网络异常'), - icon: 'none' - }) + } + }) + }, + + // 处理选择的文件(提取公共逻辑) + async handleSelectedFiles(tempFiles) { + // 显示上传进度 + uni.showLoading({ + title: '上传中...', + mask: true + }) + + let successCount = 0 + let totalCount = tempFiles.length + + for (let file of tempFiles) { + // 检查文件类型和扩展名 + const isValidPDF = this.isValidPDFFile(file) + + if (isValidPDF) { + try { + // 立即上传PDF文件到服务器(使用通用文档上传接口) + const uploadResult = await this.uploadPdfFile(file) + if (uploadResult && uploadResult.code === 1) { + const pdfFile = { + id: Date.now() + Math.random(), + name: file.name, + size: file.size, + url: uploadResult.data.url, // 使用标准响应中的url字段 + server_path: uploadResult.data.url, // 服务器可访问路径 + upload_time: new Date().toLocaleString(), + ext: uploadResult.data.ext || 'pdf', + original_name: uploadResult.data.name || file.name } + this.recordData.pdf_files.push(pdfFile) + successCount++ } else { + console.error('文件上传失败:', uploadResult) uni.showToast({ - title: '请选择PDF格式文件', + title: uploadResult.msg || '文件上传失败', icon: 'none' }) } - } - - uni.hideLoading() - - // 显示上传结果 - if (successCount > 0) { + } catch (error) { + console.error('上传PDF文件失败:', error) uni.showToast({ - title: `成功上传 ${successCount}/${totalCount} 个文件`, - icon: 'success' + title: '文件上传失败: ' + (error.msg || error.message || '网络异常'), + icon: 'none' }) } - }, - fail: (err) => { - console.error('选择文件失败:', err) + } else { uni.showToast({ - title: '选择文件失败', + title: '请选择PDF格式文件', icon: 'none' }) } - }) + } + + uni.hideLoading() + + // 显示上传结果 + if (successCount > 0) { + uni.showToast({ + title: `成功上传 ${successCount}/${totalCount} 个文件`, + icon: 'success' + }) + } + }, + + // 验证文件是否为有效的PDF文件 + isValidPDFFile(file) { + // 检查MIME类型 + if (file.type && file.type === 'application/pdf') { + return true + } + + // 检查文件扩展名(作为备用验证) + const fileName = file.name || '' + const extension = fileName.toLowerCase().split('.').pop() + if (extension === 'pdf') { + return true + } + + return false }, // 上传PDF文件到服务器(使用通用文档上传接口) diff --git a/uniapp/components/student-edit-popup/student-edit-popup.vue b/uniapp/components/student-edit-popup/student-edit-popup.vue index f782b5b9..5de1a87b 100644 --- a/uniapp/components/student-edit-popup/student-edit-popup.vue +++ b/uniapp/components/student-edit-popup/student-edit-popup.vue @@ -220,10 +220,21 @@ export default { }, methods: { // 打开弹窗 - 新增模式 - openAdd() { + openAdd(clientData = null) { this.isEditing = false this.resetStudentData() this.studentData.user_id = this.resourceId + + // 如果传递了客户信息,自动填充相关字段 + if (clientData) { + if (clientData.contact_phone) { + this.studentData.contact_phone = clientData.contact_phone + } + if (clientData.emergency_contact) { + this.studentData.emergency_contact = clientData.emergency_contact + } + } + this.$refs.popup.open() }, diff --git a/uniapp/pages-coach/coach/student/student_list.vue b/uniapp/pages-coach/coach/student/student_list.vue index ef3b04f2..b46f81ed 100644 --- a/uniapp/pages-coach/coach/student/student_list.vue +++ b/uniapp/pages-coach/coach/student/student_list.vue @@ -306,11 +306,13 @@ import apiRoute from '@/api/apiRoute.js'; background-color: #18181c; display: flex; flex-direction: column; + min-height: 100vh; } .safe-area { padding-top: var(--status-bar-height); padding-bottom: 120rpx; + flex: 1; } .search-bar { @@ -335,6 +337,7 @@ import apiRoute from '@/api/apiRoute.js'; .content { padding: 20rpx; + background-color: #18181c; } .empty-box { diff --git a/uniapp/pages-common/contract/contract_sign.vue b/uniapp/pages-common/contract/contract_sign.vue index 5e6538f9..fce59bcc 100644 --- a/uniapp/pages-common/contract/contract_sign.vue +++ b/uniapp/pages-common/contract/contract_sign.vue @@ -112,7 +112,7 @@ @@ -874,6 +925,44 @@ display: flex; gap: 16rpx; justify-content: flex-end; + + .action_button { + padding: 16rpx 32rpx; + border-radius: 8rpx; + font-size: 24rpx; + text-align: center; + color: #fff; + cursor: pointer; + transition: all 0.3s ease; + border: none; + + &.secondary_button { + background: #f8f9fa; + color: #666; + + &:active { + background: #e9ecef; + } + } + + &.primary_button { + background: #29D3B4; + color: #fff; + + &:active { + background: #26c6a0; + } + } + + &.sign_button { + background: #ff6b35; + color: #fff; + + &:active { + background: #e55a2b; + } + } + } } } } @@ -882,6 +971,27 @@ // 加载更多 .load_more_section { padding: 40rpx 20rpx 80rpx; + + .load_more_button { + padding: 24rpx 32rpx; + background: transparent; + color: #666; + font-size: 28rpx; + text-align: center; + border: 1rpx solid #e0e0e0; + border-radius: 8rpx; + cursor: pointer; + transition: all 0.3s ease; + + &:active { + background: #f5f5f5; + } + + &.loading { + color: #999; + cursor: not-allowed; + } + } } // 合同详情弹窗 @@ -979,8 +1089,33 @@ gap: 16rpx; border-top: 1px solid #f0f0f0; - fui-button { + .popup_button { flex: 1; + padding: 16rpx 24rpx; + border-radius: 8rpx; + font-size: 28rpx; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + border: none; + + &.primary_popup_button { + background: #3498db; + color: #fff; + + &:active { + background: #2980b9; + } + } + + &.secondary_popup_button { + background: #f8f9fa; + color: #666; + + &:active { + background: #e9ecef; + } + } } } } diff --git a/uniapp/pages/student/profile/index.vue b/uniapp/pages/student/profile/index.vue index 04ca56ed..d14a36fc 100644 --- a/uniapp/pages/student/profile/index.vue +++ b/uniapp/pages/student/profile/index.vue @@ -60,7 +60,8 @@