diff --git a/admin/src/app/api/lesson_course_teaching.ts b/admin/src/app/api/lesson_course_teaching.ts index 05234155..6f437968 100644 --- a/admin/src/app/api/lesson_course_teaching.ts +++ b/admin/src/app/api/lesson_course_teaching.ts @@ -339,4 +339,69 @@ export function setBindingTestPaperModule(params: Record) { ) } +/** + * 获取模块配置列表(用于动态Tab) + * @returns + */ +export function getModuleConfigs() { + return request.get(`lesson_course_teaching/module_configs`) +} + +/** + * 统一获取列表数据(支持所有table_type) + * @param params + * @returns + */ +export function getUnifiedList(params: Record) { + return request.get(`lesson_course_teaching/unified_list`, { + params, + }) +} + +/** + * 统一添加数据(支持所有table_type) + * @param params + * @returns + */ +export function addUnified(params: Record) { + return request.post('lesson_course_teaching/unified', params, { + showErrorMessage: true, + showSuccessMessage: true, + }) +} + +/** + * 统一编辑数据(支持所有table_type) + * @param params + * @returns + */ +export function editUnified(params: Record) { + return request.put( + `lesson_course_teaching/unified/${params.id}`, + params, + { showErrorMessage: true, showSuccessMessage: true } + ) +} + +/** + * 统一删除数据(支持所有table_type) + * @param id + * @returns + */ +export function deleteUnified(id: number) { + return request.delete(`lesson_course_teaching/unified/${id}`, { + showErrorMessage: true, + showSuccessMessage: true, + }) +} + +/** + * 统一获取详情数据(支持所有table_type) + * @param id + * @returns + */ +export function getUnifiedInfo(id: number) { + return request.get(`lesson_course_teaching/unified/${id}`) +} + // USER_CODE_END -- lesson_course_teaching diff --git a/admin/src/app/views/binding_personnel/binding_personnel.vue b/admin/src/app/views/binding_personnel/binding_personnel.vue index e8d22fb3..763e78ff 100644 --- a/admin/src/app/views/binding_personnel/binding_personnel.vue +++ b/admin/src/app/views/binding_personnel/binding_personnel.vue @@ -193,6 +193,7 @@ import { addLessonCourseTeaching, editLessonCourseTeaching, getLessonCourseTeachingInfo, + getUnifiedInfo, getLessonCourseTeachingList, getWithPersonnelDataList, setBindingModule, @@ -223,18 +224,71 @@ let lessonCourseTeachingTable = reactive({ const lessonCourseTableRef = ref() +const boundUserIds = ref([]) // 初始绑定的用户ID +const selectedUserIds = ref>(new Set()) // 当前选中的用户ID集合 + +// 是否禁用自动全选(避免干扰用户手动选择) +const disableAutoSelect = ref(false) +// 用于跟踪已访问的页面和选择状态 +const visitedPages = ref(new Map>()) // 页码 -> 该页面选中的用户ID集合 +// 标志位:是否正在恢复选择状态(防止状态恢复过程中的冲突) +const isRestoringSelection = ref(false) + watch( () => lessonCourseTeachingTable.data, async (newData) => { if (newData.length > 0) { await nextTick() - newData.forEach((row) => { - lessonCourseTableRef.value.toggleRowSelection(row, false) - if(lessonCourseTeachingTable.searchParam.dept_id || lessonCourseTeachingTable.searchParam.role_id){ - lessonCourseTableRef.value.toggleRowSelection(row, true) - } + + console.log('数据变更 - 开始处理页面:', lessonCourseTeachingTable.page) + console.log('数据变更 - 恢复前全局状态:', Array.from(selectedUserIds.value)) + + // 设置恢复模式标志,防止清除选中状态时影响全局状态 + isRestoringSelection.value = true + console.log('数据变更 - 设置恢复模式标志为 true') + + // 先清除所有选中(此时isRestoringSelection为true,避免误删全局状态) + if (lessonCourseTableRef.value) { + console.log('数据变更 - 执行clearSelection前,恢复标志:', isRestoringSelection.value) + lessonCourseTableRef.value.clearSelection() + console.log('数据变更 - clearSelection执行完成') + } + + // 等待多个DOM更新周期,确保clearSelection相关的所有异步事件处理完成 + await nextTick() + await nextTick() + await nextTick() + + // 如果有搜索条件且未禁用自动选择,全选当前页面 + if(!disableAutoSelect.value && (lessonCourseTeachingTable.searchParam.dept_id || lessonCourseTeachingTable.searchParam.role_id)){ + newData.forEach((row) => { + lessonCourseTableRef.value.toggleRowSelection(row, true) + selectedUserIds.value.add(row.sys_user_id) // 记录选中状态 + }) - }) + // 记录当前页面的选中状态 + const currentPageIds = new Set(newData.map(row => row.sys_user_id)) + visitedPages.value.set(lessonCourseTeachingTable.page, currentPageIds) + + // 设置自动选择后禁用标志,避免翻页时重复全选 + disableAutoSelect.value = true + } else { + // 恢复跨页选中状态 + console.log('数据变更 - 开始恢复跨页选中状态,恢复标志:', isRestoringSelection.value) + restorePageSelections(newData) + } + + console.log('数据变更 - 恢复后全局状态:', Array.from(selectedUserIds.value)) + + // 确保所有状态恢复操作完成后,再重置恢复标志 + await nextTick() + await nextTick() + await nextTick() + + // 最终重置恢复标志 + console.log('数据变更 - 准备重置恢复标志') + isRestoringSelection.value = false + console.log('数据变更 - 恢复标志重置完成,最终全局状态:', Array.from(selectedUserIds.value)) } }, { deep: true } @@ -256,10 +310,66 @@ const multipleSelection = ref<[]>([]) const binding_module = ref('') const handleSelectionChange = (val: []) => { multipleSelection.value = val + + // 获取当前页面的用户ID列表 + const currentPageUserIds = lessonCourseTeachingTable.data.map((row: any) => row.sys_user_id) + + // 获取当前页面实际选中的用户ID + const currentSelectedIds = val.map((row: any) => row.sys_user_id) + + console.log('选择变更事件 - 页面:', lessonCourseTeachingTable.page, '恢复标志:', isRestoringSelection.value) + console.log('选择变更事件 - 选中ID:', currentSelectedIds) + console.log('选择变更事件 - 变更前全局状态:', Array.from(selectedUserIds.value)) + + // 如果正在恢复选择状态,使用安全的更新逻辑 + if (isRestoringSelection.value) { + console.log('恢复模式 - 安全模式,只添加不删除') + + // 只添加新选中的用户,不删除任何用户(避免在恢复过程中误删) + currentSelectedIds.forEach(userId => { + selectedUserIds.value.add(userId) + }) + + // 更新当前页面的访问记录 + const currentPageSelections = new Set(currentSelectedIds) + visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections) + + console.log('恢复模式 - 全局选中状态:', Array.from(selectedUserIds.value)) + console.log('恢复模式 - 页面访问记录:', Object.fromEntries(visitedPages.value)) + return + } + + // 正常的用户选择逻辑:精确更新全局选中状态,只处理当前页面的选中状态变化 + console.log('正常模式 - 精确更新全局状态') + + // 移除当前页面不再选中的用户ID + currentPageUserIds.forEach(userId => { + if (!currentSelectedIds.includes(userId)) { + console.log('正常模式 - 删除用户ID:', userId) + selectedUserIds.value.delete(userId) + } + }) + + // 添加当前页面新选中的用户ID + currentSelectedIds.forEach(userId => { + console.log('正常模式 - 添加用户ID:', userId) + selectedUserIds.value.add(userId) + }) + + // 更新当前页面的访问记录 + const currentPageSelections = new Set(currentSelectedIds) + visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections) + + console.log('正常模式 - 最终状态:') + console.log(' 当前页面:', lessonCourseTeachingTable.page) + console.log(' 当前页面用户ID:', currentPageUserIds) + console.log(' 当前页面选中ID:', currentSelectedIds) + console.log(' 访问过的页面状态:', Object.fromEntries(visitedPages.value)) + console.log(' 全局选中状态:', Array.from(selectedUserIds.value)) } const loadLessonCourseTeachingList = (page: number = 1) => { - multipleSelection.value = []; + // 不要清空选中状态,让watch处理 lessonCourseTeachingTable.loading = true lessonCourseTeachingTable.page = page @@ -282,6 +392,12 @@ loadLessonCourseTeachingList() const resetForm = (page: number = 1) => { lessonCourseTeachingTable.searchParam.name = '' lessonCourseTeachingTable.searchParam.phone = '' + lessonCourseTeachingTable.searchParam.dept_id = '' + lessonCourseTeachingTable.searchParam.role_id = '' + // 重置状态标志 + disableAutoSelect.value = false + visitedPages.value.clear() // 清空页面访问记录 + isRestoringSelection.value = false // 重置恢复标志 loadLessonCourseTeachingList() } @@ -322,34 +438,77 @@ const formRules = computed(() => { const emit = defineEmits(['complete']) +// 获取所有选中用户的完整信息 +const getAllSelectedUsers = async () => { + if (selectedUserIds.value.size === 0) { + return [] + } + + const selectedIds = Array.from(selectedUserIds.value) + console.log('准备获取所有选中用户信息,ID列表:', selectedIds) + + try { + // 获取所有人员数据(不分页) + const res = await getWithPersonnelDataList({ limit: 100 }) + const allUsers = res.data.data || [] + + // 筛选出选中的用户 + const selectedUsers = allUsers.filter(user => selectedIds.includes(user.sys_user_id)) + + console.log('获取到的选中用户信息:', selectedUsers.map(u => ({name: u.name, id: u.sys_user_id}))) + + return selectedUsers + } catch (error) { + console.error('获取用户信息失败:', error) + return [] + } +} + /** * 确认 * @param formEl */ const confirm = async (formEl: FormInstance | undefined) => { - if(multipleSelection.value.length == 0) { + if(selectedUserIds.value.size === 0) { ElMessage.error('请选择数据'); return; } - let data = { - id: BindingId.value, - user_permission: multipleSelection.value - .map((item) => item.sys_user_id) - .join(','), - user_permission_name: multipleSelection.value - .map((item) => item.name) - .join(','), - table_type: binding_module.value, - } - setBindingModule(data) - .then((res) => { + + loading.value = true + + try { + // 获取所有选中用户的完整信息 + const allSelectedUsers = await getAllSelectedUsers() + + if (allSelectedUsers.length === 0) { + ElMessage.error('获取选中用户信息失败'); loading.value = false - showDialog.value = false - emit('complete') - }) - .catch((err) => { - loading.value = false - }) + return; + } + + const data = { + id: BindingId.value, + user_permission: allSelectedUsers + .map((item) => item.sys_user_id) + .join(','), + user_permission_name: allSelectedUsers + .map((item) => item.name) + .join(','), + table_type: binding_module.value, + } + + console.log('提交的绑定数据:', data) + + await setBindingModule(data) + + loading.value = false + showDialog.value = false + emit('complete') + + } catch (err) { + console.error('保存失败:', err) + loading.value = false + } } // 获取字典数据 @@ -387,6 +546,100 @@ setUserPermissionList() const setFormData = async (row: any = null) => { BindingId.value = row.id binding_module.value = row.table_type + + // 重置状态 + boundUserIds.value = [] + selectedUserIds.value.clear() + visitedPages.value.clear() // 清空页面访问记录 + disableAutoSelect.value = false + isRestoringSelection.value = false // 重置恢复标志 + + // 获取已绑定的人员信息 + if (row.id) { + try { + // 优先使用统一详情接口 + let res + try { + res = await getUnifiedInfo(row.id) + } catch (e) { + // 如果统一接口失败,使用旧接口 + res = await getLessonCourseTeachingInfo(row.id) + } + const detailInfo = res.data + + // 如果有已绑定的人员,保存用户ID列表 + if (detailInfo.user_permission) { + boundUserIds.value = detailInfo.user_permission.split(',').map(id => parseInt(id.trim())) + + // 初始化选中状态 + boundUserIds.value.forEach(id => selectedUserIds.value.add(id)) + + console.log('已绑定的用户ID:', boundUserIds.value) + console.log('初始选中状态:', Array.from(selectedUserIds.value)) + + // 如果表格数据已经加载,直接进行回显 + if (lessonCourseTeachingTable.data.length > 0) { + setTimeout(() => { + preSelectBoundUsers(boundUserIds.value) + }, 100) + } + } + } catch (error) { + console.error('获取人员绑定信息失败:', error) + } + } +} + +// 恢复页面选中状态 +const restorePageSelections = (pageData: any[]) => { + if (!lessonCourseTableRef.value || !pageData.length) return + + let restoredCount = 0 + + pageData.forEach((row: any) => { + // 如果该用户ID在全局选中状态中,则选中该行 + if (selectedUserIds.value.has(row.sys_user_id)) { + lessonCourseTableRef.value.toggleRowSelection(row, true) + restoredCount++ + console.log('恢复选中用户:', row.name, 'ID:', row.sys_user_id) + } + }) + + // 更新当前页面的访问记录 + const currentPageSelections = new Set( + pageData + .filter(row => selectedUserIds.value.has(row.sys_user_id)) + .map(row => row.sys_user_id) + ) + visitedPages.value.set(lessonCourseTeachingTable.page, currentPageSelections) + + console.log('恢复选中状态完成,当前页面:', lessonCourseTeachingTable.page) + console.log('恢复的选中用户数:', restoredCount) + console.log('当前页面选中状态:', Array.from(currentPageSelections)) +} + +// 预选已绑定的用户 +const preSelectBoundUsers = (userIds: number[]) => { + console.log('开始预选用户,用户ID列表:', userIds) + console.log('表格数据:', lessonCourseTeachingTable.data) + + if (lessonCourseTableRef.value && lessonCourseTeachingTable.data.length > 0) { + let selectedCount = 0 + + lessonCourseTeachingTable.data.forEach((row: any) => { + // 如果该用户ID在已绑定的ID列表中,则选中该行 + if (userIds.includes(row.sys_user_id)) { + console.log('选中用户:', row.name, 'ID:', row.sys_user_id) + lessonCourseTableRef.value.toggleRowSelection(row, true) + selectedCount++ + } + }) + + console.log('预选完成,共选中', selectedCount, '个用户') + console.log('当前selectedUserIds状态:', Array.from(selectedUserIds.value)) + } else { + console.log('表格引用或数据不存在,无法进行预选') + } } // 验证手机号格式 diff --git a/admin/src/app/views/customer_resources/components/Messages.vue b/admin/src/app/views/customer_resources/components/Messages.vue new file mode 100644 index 00000000..e534cbaa --- /dev/null +++ b/admin/src/app/views/customer_resources/components/Messages.vue @@ -0,0 +1,359 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/customer_resources/components/UserProfile.vue b/admin/src/app/views/customer_resources/components/UserProfile.vue index b52ba956..e05026d4 100644 --- a/admin/src/app/views/customer_resources/components/UserProfile.vue +++ b/admin/src/app/views/customer_resources/components/UserProfile.vue @@ -30,6 +30,7 @@ + @@ -90,6 +91,10 @@ + + + + @@ -102,6 +107,7 @@ import Orders from '@/app/views/customer_resources/components/order_table.vue' import CommunicationRecords from '@/app/views/communication_records/communication_records.vue' import GiftRecords from '@/app/views/customer_resources/components/gift_records.vue' + import Messages from '@/app/views/customer_resources/components/Messages.vue' let showDialog = ref(false) diff --git a/admin/src/app/views/jlyj/jlyj.vue b/admin/src/app/views/jlyj/jlyj.vue index 0bea7042..439a988d 100644 --- a/admin/src/app/views/jlyj/jlyj.vue +++ b/admin/src/app/views/jlyj/jlyj.vue @@ -3,200 +3,246 @@
{{ pageName }} - 新增阶段
+ + + + + + + + + + + + + + + + + + + + + + + + -
- 基础绩效配置 -
-
- 阶段名称 - 底薪 -
+ - - - diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/Jump-lesson-library-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/Jump-lesson-library-edit.vue new file mode 100644 index 00000000..02749b1e --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/Jump-lesson-library-edit.vue @@ -0,0 +1,284 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/basketball-course-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/basketball-course-teaching-edit.vue new file mode 100644 index 00000000..a2ab25f9 --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/basketball-course-teaching-edit.vue @@ -0,0 +1,284 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/en-course-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/en-course-teaching-edit.vue new file mode 100644 index 00000000..eb18c55b --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/en-course-teaching-edit.vue @@ -0,0 +1,280 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/lesson-course-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/lesson-course-teaching-edit.vue new file mode 100644 index 00000000..8a35ab1f --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/lesson-course-teaching-edit.vue @@ -0,0 +1,284 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/ninja-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/ninja-teaching-edit.vue new file mode 100644 index 00000000..eca8487c --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/ninja-teaching-edit.vue @@ -0,0 +1,280 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/physical-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/physical-teaching-edit.vue new file mode 100644 index 00000000..e2cab358 --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/physical-teaching-edit.vue @@ -0,0 +1,284 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/security-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/security-teaching-edit.vue new file mode 100644 index 00000000..647ef217 --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/security-teaching-edit.vue @@ -0,0 +1,284 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/components_backup/strengthen-course-teaching-edit.vue b/admin/src/app/views/lesson_course_teaching/components/components_backup/strengthen-course-teaching-edit.vue new file mode 100644 index 00000000..1b8e6bec --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/components_backup/strengthen-course-teaching-edit.vue @@ -0,0 +1,282 @@ + + + + + + diff --git a/admin/src/app/views/lesson_course_teaching/components/unified-data-table.vue b/admin/src/app/views/lesson_course_teaching/components/unified-data-table.vue new file mode 100644 index 00000000..b0570289 --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/unified-data-table.vue @@ -0,0 +1,344 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/lesson_course_teaching/components/unified-edit-dialog.vue b/admin/src/app/views/lesson_course_teaching/components/unified-edit-dialog.vue new file mode 100644 index 00000000..fd8d660f --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/components/unified-edit-dialog.vue @@ -0,0 +1,371 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue b/admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue index d68c61a6..3c16bc0c 100644 --- a/admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue +++ b/admin/src/app/views/lesson_course_teaching/lesson_course_teaching.vue @@ -1,2236 +1,373 @@ -/** - * 删除教研管理 - */ -const deleteEvent = (id: number, type: number) => { - ElMessageBox.confirm(t('lessonCourseTeachingDeleteTips'), t('warning'), { - confirmButtonText: t('confirm'), - cancelButtonText: t('cancel'), - type: 'warning', - }).then(() => { - deleteLessonCourseTeaching(id) - .then(() => { - if (type === 1) { - loadLessonCourseTeachingList() - } else if (type === 2) { - loadJumpLessonLibraryList() - } else if (type === 3) { - loadEnTeachingLibraryList() - } else if (type === 4) { - loadBasketballTeachingLibraryList() - } else if (type === 5) { - loadStrengthenTeachingLibraryList() - } else if (type === 6) { - loadNinjaTeachingLibraryList() - } else if (type === 7) { - loadSecurityTeachingLibraryList() - } else if (type === 8) { - loadPhysicalTeachingLibraryList() - } - }) - .catch(() => {}) - }) + + \ No newline at end of file diff --git a/admin/src/app/views/lesson_course_teaching/lesson_course_teaching_backup.vue b/admin/src/app/views/lesson_course_teaching/lesson_course_teaching_backup.vue new file mode 100644 index 00000000..d68c61a6 --- /dev/null +++ b/admin/src/app/views/lesson_course_teaching/lesson_course_teaching_backup.vue @@ -0,0 +1,2236 @@ + + + + + diff --git a/doc/副本(时间卡)体能课学员课程协议.docx b/doc/副本(时间卡)体能课学员课程协议.docx index 83cedddd..044aec7d 100644 Binary files a/doc/副本(时间卡)体能课学员课程协议.docx and b/doc/副本(时间卡)体能课学员课程协议.docx differ diff --git a/niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php b/niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php index 6021677b..0ab918d7 100644 --- a/niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php +++ b/niucloud/app/adminapi/controller/lesson_course_teaching/LessonCourseTeaching.php @@ -780,4 +780,160 @@ class LessonCourseTeaching extends BaseAdminController return success('ADD_SUCCESS', ['id' => $id]); } + // =================== 统一API接口:支持所有模块类型 =================== + + /** + * 统一查询接口 - 根据table_type参数返回对应类型的数据 + * @return \think\Response + */ + public function unifiedList(){ + $data = $this->request->params([ + ["title",""], + ["status",""], + ["create_time",["",""]], + ["update_time",["",""]], + ["table_type",""] // 必填参数,用于区分模块类型 + ]); + + // 验证table_type参数 + if (empty($data['table_type'])) { + return error('table_type参数不能为空'); + } + + return success((new LessonCourseTeachingService())->getPage($data)); + } + + /** + * 统一新增接口 - 支持所有模块类型的数据新增 + * @return \think\Response + */ + public function unifiedAdd(){ + $data = $this->request->params([ + ["title",""], + ["image",""], + ["type",0], + ["content",""], + ["status",0], + ["table_type",""], // 必填参数,用于区分模块类型 + ["url",""], + ["exam_papers_id",""], + ["user_permission",[]] // 支持数组格式的权限数据 + ]); + + // 验证table_type参数 + if (empty($data['table_type'])) { + return error('table_type参数不能为空'); + } + + $this->validate($data, 'app\validate\lesson_course_teaching\LessonCourseTeaching.add'); + $id = (new LessonCourseTeachingService())->add($data); + return success('ADD_SUCCESS', ['id' => $id]); + } + + /** + * 统一编辑接口 - 支持所有模块类型的数据编辑 + * @param int $id + * @return \think\Response + */ + public function unifiedEdit(int $id){ + $data = $this->request->params([ + ["title",""], + ["image",""], + ["type",0], + ["content",""], + ["status",0], + ["url",""], + ["exam_papers_id",""], + ["user_permission",[]] // 支持数组格式的权限数据 + ]); + + $this->validate($data, 'app\validate\lesson_course_teaching\LessonCourseTeaching.edit'); + (new LessonCourseTeachingService())->edit($id, $data); + return success('EDIT_SUCCESS'); + } + + /** + * 统一删除接口 - 支持所有模块类型的数据删除 + * @param int $id + * @return \think\Response + */ + public function unifiedDel(int $id){ + (new LessonCourseTeachingService())->del($id); + return success('DELETE_SUCCESS'); + } + + /** + * 统一详情接口 - 支持所有模块类型的数据详情获取 + * @param int $id + * @return \think\Response + */ + public function unifiedInfo(int $id){ + return success((new LessonCourseTeachingService())->getInfo($id)); + } + + /** + * 获取模块配置接口 - 返回所有可用的模块配置 + * @return \think\Response + */ + public function getModuleConfigs(){ + $config = config('teaching_management.teaching_management.module_configs', []); + + // 按order排序并格式化返回数据 + $modules = []; + foreach ($config as $key => $moduleConfig) { + $modules[] = [ + 'key' => $key, + 'table_type' => $moduleConfig['table_type'], + 'name' => $moduleConfig['name'], + 'auto_distribute' => $moduleConfig['auto_distribute'] ?? false, + 'order' => $moduleConfig['order'] ?? 99 + ]; + } + + // 按order字段排序 + usort($modules, function($a, $b) { + return $a['order'] <=> $b['order']; + }); + + return success([ + 'modules' => $modules, + 'coach_department_id' => config('teaching_management.teaching_management.coach_department_id', 24) + ]); + } + + /** + * 批量更新人员权限接口 - 手动触发教练部人员权限同步 + * @return \think\Response + */ + public function batchUpdatePersonnel(){ + $data = $this->request->params([ + ["table_types",[]] // 可选:指定要更新的table_type数组,为空则更新所有自动分发的模块 + ]); + + $tableTypes = !empty($data['table_types']) ? $data['table_types'] : null; + $result = (new LessonCourseTeachingService())->batchUpdatePersonnelPermissions($tableTypes); + + if ($result['failed'] > 0) { + return error('部分更新失败', $result); + } + + return success('批量更新完成', $result); + } + + /** + * 手动执行教研人员同步定时任务 - 测试定时任务功能 + * @return \think\Response + */ + public function testSyncTask(){ + try { + $job = new \app\job\schedule\TeachingPersonnelSync(); + $result = $job->doJob(); + + return success('定时任务执行完成', ['result' => $result]); + + } catch (\Exception $e) { + return error('定时任务执行失败: ' . $e->getMessage()); + } + } + } diff --git a/niucloud/app/adminapi/controller/performance/PerformanceConfig.php b/niucloud/app/adminapi/controller/performance/PerformanceConfig.php new file mode 100644 index 00000000..921980f9 --- /dev/null +++ b/niucloud/app/adminapi/controller/performance/PerformanceConfig.php @@ -0,0 +1,206 @@ +request->get('config_type', ''); + + if (empty($configType)) { + return fail('配置类型不能为空'); + } + + $performanceConfigService = new PerformanceConfigService(); + $structure = $performanceConfigService->getConfigStructure($configType); + + if (!$structure) { + return fail('未找到配置类型'); + } + + return success(data: $structure); + } + + /** + * 获取配置数据 + * @return \think\Response + */ + public function getConfigData() + { + $configType = $this->request->get('config_type', ''); + + if (empty($configType)) { + return fail('配置类型不能为空'); + } + + $performanceConfigService = new PerformanceConfigService(); + $configData = $performanceConfigService->getConfigData($configType); + + return success(data: [ + 'config_type' => $configType, + 'data' => $configData + ]); + } + + /** + * 保存配置数据 + * @return \think\Response + */ + public function saveConfigData() + { + $data = $this->request->params([ + ['config_type', ''], + ['config_data', []], + ]); + + if (empty($data['config_type'])) { + return fail('配置类型不能为空'); + } + + if (empty($data['config_data'])) { + return fail('配置数据不能为空'); + } + + try { + $performanceConfigService = new PerformanceConfigService(); + $result = $performanceConfigService->saveConfigData( + $data['config_type'], + $data['config_data'], + $this->request->uid() + ); + + if ($result) { + return success(msg: '配置保存成功'); + } else { + return fail('配置保存失败'); + } + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取所有可用配置类型 + * @return \think\Response + */ + public function getConfigTypes() + { + $performanceConfigService = new PerformanceConfigService(); + $types = $performanceConfigService->getAvailableConfigTypes(); + + return success(data: $types); + } + + /** + * 重置配置为默认值 + * @return \think\Response + */ + public function resetToDefault() + { + $configType = $this->request->get('config_type', ''); + + if (empty($configType)) { + return fail('配置类型不能为空'); + } + + try { + $performanceConfigService = new PerformanceConfigService(); + $result = $performanceConfigService->resetConfigToDefault($configType, $this->request->uid()); + + if ($result) { + return success(msg: '配置已重置为默认值'); + } else { + return fail('配置重置失败'); + } + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 导出配置 + * @return \think\Response + */ + public function exportConfig() + { + $configType = $this->request->get('config_type', ''); + + if (empty($configType)) { + return fail('配置类型不能为空'); + } + + try { + $performanceConfigService = new PerformanceConfigService(); + $exportData = $performanceConfigService->exportConfig($configType); + + return success(data: $exportData); + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 导入配置 + * @return \think\Response + */ + public function importConfig() + { + $data = $this->request->params([ + ['import_data', []], + ]); + + if (empty($data['import_data'])) { + return fail('导入数据不能为空'); + } + + try { + $performanceConfigService = new PerformanceConfigService(); + $result = $performanceConfigService->importConfig($data['import_data'], $this->request->uid()); + + if ($result) { + return success(msg: '配置导入成功'); + } else { + return fail('配置导入失败'); + } + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 清除配置缓存 + * @return \think\Response + */ + public function clearCache() + { + $configType = $this->request->get('config_type', ''); + + $performanceConfigService = new PerformanceConfigService(); + $performanceConfigService->clearConfigCache($configType ?: null); + + return success(msg: '缓存清除成功'); + } + +} \ No newline at end of file diff --git a/niucloud/app/adminapi/controller/sys/System.php b/niucloud/app/adminapi/controller/sys/System.php index 138d2c9b..d9ad757c 100644 --- a/niucloud/app/adminapi/controller/sys/System.php +++ b/niucloud/app/adminapi/controller/sys/System.php @@ -188,4 +188,5 @@ class System extends BaseAdminController public function personnel_summary(){ return success(data: (new SystemService())->personnel_summary()); } + } diff --git a/niucloud/app/adminapi/route/lesson_course_teaching.php b/niucloud/app/adminapi/route/lesson_course_teaching.php index eee64fd9..3abe0aa7 100644 --- a/niucloud/app/adminapi/route/lesson_course_teaching.php +++ b/niucloud/app/adminapi/route/lesson_course_teaching.php @@ -119,6 +119,24 @@ Route::group('lesson_course_teaching', function () { Route::get('test_paper','lesson_course_teaching.LessonCourseTeaching/getTestPaperList'); Route::put('binding_test_module/:id', 'lesson_course_teaching.LessonCourseTeaching/bindingTestModule'); + // =================== 统一API接口路由 =================== + // 统一查询接口 + Route::get('unified_list', 'lesson_course_teaching.LessonCourseTeaching/unifiedList'); + // 统一新增接口 + Route::post('unified', 'lesson_course_teaching.LessonCourseTeaching/unifiedAdd'); + // 统一编辑接口 + Route::put('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedEdit'); + // 统一删除接口 + Route::delete('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedDel'); + // 统一详情接口 + Route::get('unified/:id', 'lesson_course_teaching.LessonCourseTeaching/unifiedInfo'); + // 获取模块配置接口 + Route::get('module_configs', 'lesson_course_teaching.LessonCourseTeaching/getModuleConfigs'); + // 批量更新人员权限接口 + Route::post('batch_update_personnel', 'lesson_course_teaching.LessonCourseTeaching/batchUpdatePersonnel'); + // 手动测试定时任务接口 + Route::post('test_sync_task', 'lesson_course_teaching.LessonCourseTeaching/testSyncTask'); + })->middleware([ AdminCheckToken::class, AdminCheckRole::class, diff --git a/niucloud/app/adminapi/route/performance_config.php b/niucloud/app/adminapi/route/performance_config.php new file mode 100644 index 00000000..10fad370 --- /dev/null +++ b/niucloud/app/adminapi/route/performance_config.php @@ -0,0 +1,41 @@ + input('student_id', 0), + 'from_type' => input('from_type', ''), + 'from_id' => input('from_id', 0), + 'page' => input('page', 1), + 'limit' => input('limit', 20) + ]; + + // 参数验证 + if (empty($data['student_id']) || empty($data['from_type']) || empty($data['from_id'])) { + return fail('参数错误'); + } + + // 获取对话消息 + $result = $this->messageService->getConversationMessages($data); + + return success($result); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 学员回复消息 + * @return array + */ + public function replyMessage() + { + try { + // 获取请求参数 + $data = [ + 'student_id' => input('student_id', 0), + 'to_type' => input('to_type', ''), + 'to_id' => input('to_id', 0), + 'content' => input('content', ''), + 'message_type' => input('message_type', 'text'), + 'title' => input('title', '') + ]; + + // 参数验证 + if (empty($data['student_id']) || empty($data['to_type']) || + empty($data['to_id']) || empty($data['content'])) { + return fail('参数错误'); + } + + // 发送回复 + $result = $this->messageService->replyMessage($data); + + return success($result, '回复发送成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + /** * 搜索消息 * @param int $student_id 学员ID diff --git a/niucloud/app/api/route/student.php b/niucloud/app/api/route/student.php index 2c99bce2..563cff2d 100644 --- a/niucloud/app/api/route/student.php +++ b/niucloud/app/api/route/student.php @@ -142,10 +142,14 @@ Route::group('knowledge', function () { // 消息管理(测试版本,无需token) Route::group('message-test', function () { - // 获取消息列表 + // 获取消息列表(按对话分组) Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList'); // 获取消息详情 Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail'); + // 获取对话中的所有消息 + Route::get('conversation', 'app\api\controller\student\MessageController@getConversationMessages'); + // 学员回复消息 + Route::post('reply', 'app\api\controller\student\MessageController@replyMessage'); // 标记消息已读 Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead'); // 批量标记已读 @@ -158,10 +162,14 @@ Route::group('message-test', function () { // 消息管理 Route::group('message', function () { - // 获取消息列表 + // 获取消息列表(按对话分组) Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList'); // 获取消息详情 Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail'); + // 获取对话中的所有消息 + Route::get('conversation', 'app\api\controller\student\MessageController@getConversationMessages'); + // 学员回复消息 + Route::post('reply', 'app\api\controller\student\MessageController@replyMessage'); // 标记消息已读 Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead'); // 批量标记已读 diff --git a/niucloud/app/command/TeachingSyncCommand.php b/niucloud/app/command/TeachingSyncCommand.php new file mode 100644 index 00000000..f16e8011 --- /dev/null +++ b/niucloud/app/command/TeachingSyncCommand.php @@ -0,0 +1,214 @@ +setName('teaching:sync') + ->setDescription('教研管理人员权限同步工具') + ->addArgument('action', Argument::OPTIONAL, 'sync|test|status', 'sync') + ->addOption('table-types', 't', Option::VALUE_OPTIONAL, '指定要同步的table_type,多个用逗号分隔,如:30,31,32') + ->addOption('dry-run', 'd', Option::VALUE_NONE, '仅测试不实际执行同步') + ->setHelp(' +使用示例: + php think teaching:sync # 同步所有自动分发模块 + php think teaching:sync --table-types=30,31 # 仅同步指定table_type + php think teaching:sync --dry-run # 测试模式,不实际执行 + php think teaching:sync test # 执行测试 + php think teaching:sync status # 查看配置状态 + '); + } + + protected function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + $tableTypesStr = $input->getOption('table-types'); + $dryRun = $input->getOption('dry-run'); + + $output->writeln("教研管理人员同步工具"); + $output->writeln("======================"); + + switch ($action) { + case 'sync': + $this->executeSync($input, $output, $tableTypesStr, $dryRun); + break; + case 'test': + $this->executeTest($input, $output); + break; + case 'status': + $this->showStatus($input, $output); + break; + default: + $output->writeln("未知的操作: {$action}"); + $output->writeln("支持的操作: sync, test, status"); + } + } + + /** + * 执行同步操作 + */ + private function executeSync(Input $input, Output $output, ?string $tableTypesStr, bool $dryRun) + { + try { + $tableTypes = null; + if ($tableTypesStr) { + $tableTypes = array_map('intval', explode(',', $tableTypesStr)); + $output->writeln("指定table_type: " . implode(', ', $tableTypes) . ""); + } + + if ($dryRun) { + $output->writeln("【测试模式】仅模拟执行,不实际更新数据"); + } + + $service = new LessonCourseTeachingService(); + + if ($dryRun) { + // 测试模式:仅统计待同步的记录数量 + $config = $service->getAllModuleConfigs(); + $autoDistributeModules = []; + foreach ($config as $key => $moduleConfig) { + if ($moduleConfig['auto_distribute'] ?? false) { + if ($tableTypes === null || in_array($moduleConfig['table_type'], $tableTypes)) { + $autoDistributeModules[] = [ + 'name' => $moduleConfig['name'], + 'table_type' => $moduleConfig['table_type'] + ]; + } + } + } + + $output->writeln("需要同步的模块:"); + foreach ($autoDistributeModules as $module) { + $output->writeln(" - {$module['name']} (table_type: {$module['table_type']})"); + } + + $output->writeln("测试完成,实际同步请移除 --dry-run 参数"); + return; + } + + // 实际执行同步 + $output->writeln("开始执行同步..."); + $result = $service->batchUpdatePersonnelPermissions($tableTypes); + + $output->writeln(""); + $output->writeln("同步结果:"); + $output->writeln(" 总记录数: {$result['total']}"); + $output->writeln(" 成功同步: {$result['success']}"); + $output->writeln(" 同步失败: " . ($result['failed'] > 0 ? "{$result['failed']}" : "{$result['failed']}")); + + if ($result['failed'] > 0 && !empty($result['errors'])) { + $output->writeln(""); + $output->writeln("失败详情:"); + foreach ($result['errors'] as $error) { + $output->writeln(" - {$error}"); + } + } + + if ($result['success'] > 0) { + $output->writeln(""); + $output->writeln("✓ 同步完成!"); + } + + } catch (\Exception $e) { + $output->writeln(""); + $output->writeln("同步失败: {$e->getMessage()}"); + $output->writeln("文件: {$e->getFile()}:{$e->getLine()}"); + } + } + + /** + * 执行测试 + */ + private function executeTest(Input $input, Output $output) + { + try { + $output->writeln("执行定时任务测试..."); + + $job = new TeachingPersonnelSync(); + $result = $job->doJob(); + + $output->writeln(""); + $output->writeln("测试结果:"); + $output->writeln(" {$result}"); + $output->writeln(""); + $output->writeln("✓ 测试完成!"); + + } catch (\Exception $e) { + $output->writeln(""); + $output->writeln("测试失败: {$e->getMessage()}"); + $output->writeln("文件: {$e->getFile()}:{$e->getLine()}"); + } + } + + /** + * 显示配置状态 + */ + private function showStatus(Input $input, Output $output) + { + try { + $config = config('teaching_management.teaching_management', []); + $moduleConfigs = $config['module_configs'] ?? []; + $cronConfig = $config['cron_config']['sync_personnel'] ?? []; + + $output->writeln("配置状态:"); + $output->writeln(" 教练部门ID: " . ($config['coach_department_id'] ?? 'N/A') . ""); + $output->writeln(" 定时任务启用: " . ($cronConfig['enabled'] ?? false ? '是' : '否') . ""); + $output->writeln(" 定时任务调度: " . ($cronConfig['schedule'] ?? 'N/A') . ""); + + $output->writeln(""); + $output->writeln("自动分发模块:"); + $autoModules = []; + $regularModules = []; + + foreach ($moduleConfigs as $key => $moduleConfig) { + if ($moduleConfig['auto_distribute'] ?? false) { + $autoModules[] = $moduleConfig; + } else { + $regularModules[] = $moduleConfig; + } + } + + if (!empty($autoModules)) { + foreach ($autoModules as $module) { + $output->writeln(" ✓ {$module['name']} (table_type: {$module['table_type']})"); + } + } else { + $output->writeln(" 暂无自动分发模块"); + } + + $output->writeln(""); + $output->writeln("普通模块:"); + if (!empty($regularModules)) { + foreach ($regularModules as $module) { + $output->writeln(" - {$module['name']} (table_type: {$module['table_type']})"); + } + } else { + $output->writeln(" 暂无普通模块"); + } + + } catch (\Exception $e) { + $output->writeln(""); + $output->writeln("获取状态失败: {$e->getMessage()}"); + } + } +} \ No newline at end of file diff --git a/niucloud/app/dict/schedule/schedule.php b/niucloud/app/dict/schedule/schedule.php index 9abb1b5a..095207e0 100644 --- a/niucloud/app/dict/schedule/schedule.php +++ b/niucloud/app/dict/schedule/schedule.php @@ -53,4 +53,17 @@ return [ 'class' => 'app\job\schedule\HandleCourseSchedule', 'function' => '' ], + [ + 'key' => 'teaching_personnel_sync', + 'name' => '教研管理人员同步', + 'desc' => '定时同步教练部人员权限到教研管理模块', + 'time' => [ + 'type' => 'day', + 'day' => 1, + 'hour' => 2, + 'min' => 0 + ], + 'class' => 'app\job\schedule\TeachingPersonnelSync', + 'function' => 'doJob' + ], ]; diff --git a/niucloud/app/job/schedule/TeachingPersonnelSync.php b/niucloud/app/job/schedule/TeachingPersonnelSync.php new file mode 100644 index 00000000..f0fdacf0 --- /dev/null +++ b/niucloud/app/job/schedule/TeachingPersonnelSync.php @@ -0,0 +1,112 @@ +batchUpdatePersonnelPermissions(); + + $message = sprintf( + '教研管理人员同步完成 - 总计: %d, 成功: %d, 失败: %d', + $result['total'], + $result['success'], + $result['failed'] + ); + + Log::info($message, $result); + + // 如果有失败的记录,记录详细错误信息 + if ($result['failed'] > 0 && !empty($result['errors'])) { + Log::warning('教研管理人员同步部分失败', [ + 'failed_count' => $result['failed'], + 'errors' => $result['errors'] + ]); + } + + return $message; + + } catch (\Exception $e) { + $error = '教研管理人员同步任务执行失败: ' . $e->getMessage(); + Log::error($error, [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + + return $error; + } + } + + /** + * 手动执行同步任务(用于测试) + * @param array|null $tableTypes 指定要同步的table_type数组 + * @return string + */ + public function manualSync(?array $tableTypes = null) + { + try { + Log::info('手动执行教研管理人员同步任务', ['table_types' => $tableTypes]); + + $service = new LessonCourseTeachingService(); + + // 执行指定模块的批量人员权限同步 + $result = $service->batchUpdatePersonnelPermissions($tableTypes); + + $message = sprintf( + '手动同步完成 - 总计: %d, 成功: %d, 失败: %d', + $result['total'], + $result['success'], + $result['failed'] + ); + + Log::info($message, $result); + + return $message; + + } catch (\Exception $e) { + $error = '手动同步任务执行失败: ' . $e->getMessage(); + Log::error($error, [ + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + + return $error; + } + } +} \ No newline at end of file diff --git a/niucloud/app/model/personnel/Personnel.php b/niucloud/app/model/personnel/Personnel.php index 53929764..c4592752 100644 --- a/niucloud/app/model/personnel/Personnel.php +++ b/niucloud/app/model/personnel/Personnel.php @@ -158,6 +158,24 @@ class Personnel extends BaseModel + /** + * 状态字段转化 + * @param $value + * @param $data + * @return string + */ + public function getStatusNameAttr($value, $data) + { + if (empty($data['status']) && $data['status'] !== 0) return ''; + + return match ($data['status']) { + self::STATUS_NORMAL => '正常', + self::STATUS_DISABLED => '禁用', + self::STATUS_PENDING_APPROVAL => '待审批', + default => '未知状态' + }; + } + public function sys_user() { return $this->hasOne(\app\model\sys\SysUser::class, 'uid', 'sys_user_id'); diff --git a/niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php b/niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php index 39cb502e..ad5822da 100644 --- a/niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php +++ b/niucloud/app/service/admin/lesson_course_teaching/LessonCourseTeachingService.php @@ -14,614 +14,688 @@ namespace app\service\admin\lesson_course_teaching; use app\model\lesson_course_teaching\LessonCourseTeaching; use app\model\personnel_data\PersonnelData; use app\model\exam_papers\ExamPapers; - +use think\facade\Db; +use think\facade\Log; use core\base\BaseAdminService; - /** - * 教研管理服务层 + * 教研管理服务层 - 重构版本支持统一管理和自动分发 * Class LessonCourseTeachingService * @package app\service\admin\lesson_course_teaching */ class LessonCourseTeachingService extends BaseAdminService { + /** + * 教研管理配置 + * @var array + */ + private $config; + public function __construct() { parent::__construct(); $this->model = new LessonCourseTeaching(); + + // 加载教研管理配置 + $this->config = config('teaching_management.teaching_management', []); + } + + /** + * 获取模块配置 + * @param string|null $moduleKey + * @return array|null + */ + private function getModuleConfig(?string $moduleKey = null) + { + if ($moduleKey) { + return $this->config['module_configs'][$moduleKey] ?? null; + } + return $this->config['module_configs'] ?? []; + } + + /** + * 根据table_type获取模块配置 + * @param int $tableType + * @return array|null + */ + private function getModuleConfigByTableType(int $tableType): ?array + { + $configs = $this->getModuleConfig(); + foreach ($configs as $key => $config) { + if ($config['table_type'] === $tableType) { + return $config; + } + } + return null; } /** - * 获取课程教学大纲列表 + * 获取所有模块配置(公共方法,供外部调用) + * @return array + */ + public function getAllModuleConfigs(): array + { + return $this->getModuleConfig(); + } + + /** + * 统一获取列表数据 * @param array $where * @return array */ public function getPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; + $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,user_permission_name,exam_papers_id'; $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); + $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where) + ->field($field) + ->order($order); + $list = $this->pageQuery($search_model); + + // 处理用户权限回显数据 + if (!empty($list['data'])) { + foreach ($list['data'] as &$item) { + $item['user_permission_list'] = $this->parseUserPermissionToList($item['user_permission']); + } + } + return $list; } - /** - * 添加课程教学大纲 - * @param array $data - * @return mixed + * 解析用户权限字段为列表格式 + * @param string|null $userPermission + * @return array */ - public function add(array $data) + private function parseUserPermissionToList(?string $userPermission): array { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); + if (empty($userPermission)) { + return []; } - $res = $this->model->create($data); - return $res->id; + try { + $personIds = array_filter(explode(',', $userPermission)); + if (empty($personIds)) { + return []; + } + + // 获取人员详细信息 + $personnel = Db::table('school_personnel') + ->whereIn('id', $personIds) + ->where('deleted_at', 0) + ->field('id,name,phone') + ->select() + ->toArray(); + + return $personnel; + } catch (\Exception $e) { + Log::error("解析用户权限失败: " . $e->getMessage()); + return []; + } } /** - * 课程教学大纲编辑 + * 统一新增数据 + * @param array $data + * @return mixed + */ + public function add(array $data) + { + try { + // 处理用户权限字段 + if (isset($data['user_permission']) && is_array($data['user_permission'])) { + $data['user_permission'] = implode(',', $data['user_permission']); + } + + // 检查是否需要自动分发 + $tableType = $data['table_type'] ?? 0; + $moduleConfig = $this->getModuleConfigByTableType($tableType); + + if ($moduleConfig && ($moduleConfig['auto_distribute'] ?? false)) { + $autoPermissions = $this->getCoachPersonnel(); + if (!empty($autoPermissions)) { + // 合并用户指定的权限和自动分发的权限 + $existingPermissions = !empty($data['user_permission']) ? explode(',', $data['user_permission']) : []; + $allPermissions = array_unique(array_merge($existingPermissions, $autoPermissions['person_ids'])); + + $data['user_permission'] = implode(',', $allPermissions); + $data['user_permission_name'] = implode(',', $autoPermissions['person_names']); + + Log::info("自动分发人员权限", [ + 'table_type' => $tableType, + 'module' => $moduleConfig['name'], + 'permissions' => $data['user_permission'], + 'names' => $data['user_permission_name'] + ]); + } + } + + $res = $this->model->create($data); + return $res->id; + + } catch (\Exception $e) { + Log::error("添加教研管理数据失败: " . $e->getMessage()); + throw new \Exception("添加失败: " . $e->getMessage()); + } + } + + /** + * 统一编辑数据 * @param int $id * @param array $data * @return bool */ public function edit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); + try { + // 获取现有记录 + $existingRecord = $this->model->where('id', $id)->find(); + if (!$existingRecord) { + throw new \Exception("记录不存在"); + } + + // 处理用户权限字段 + if (isset($data['user_permission']) && is_array($data['user_permission'])) { + $data['user_permission'] = implode(',', $data['user_permission']); + } + + // 检查是否需要重新同步自动分发权限 + $tableType = $existingRecord['table_type']; + $moduleConfig = $this->getModuleConfigByTableType($tableType); + + if ($moduleConfig && ($moduleConfig['auto_distribute'] ?? false)) { + $autoPermissions = $this->getCoachPersonnel(); + if (!empty($autoPermissions)) { + // 获取当前用户指定的权限 + $userPermissions = !empty($data['user_permission']) ? explode(',', $data['user_permission']) : []; + + // 获取之前自动分发的权限(从现有记录中获取) + $existingPermissions = !empty($existingRecord['user_permission']) ? explode(',', $existingRecord['user_permission']) : []; + + // 重新合并:用户权限 + 最新的教练部人员 + $allPermissions = array_unique(array_merge($userPermissions, $autoPermissions['person_ids'])); + + $data['user_permission'] = implode(',', $allPermissions); + $data['user_permission_name'] = implode(',', $autoPermissions['person_names']); + + Log::info("编辑时重新同步人员权限", [ + 'id' => $id, + 'table_type' => $tableType, + 'module' => $moduleConfig['name'], + 'old_permissions' => $existingRecord['user_permission'], + 'new_permissions' => $data['user_permission'] + ]); + } + } + + $this->model->where('id', $id)->update($data); + return true; + + } catch (\Exception $e) { + Log::error("编辑教研管理数据失败: " . $e->getMessage()); + throw new \Exception("编辑失败: " . $e->getMessage()); + } + } + + /** + * 获取教练部人员列表(用于自动分发) + * @return array|null + */ + private function getCoachPersonnel(): ?array + { + try { + $coachDeptId = $this->config['coach_department_id'] ?? 24; + + // 1. 查询教练部对应的角色 + $roles = Db::table('school_sys_role') + ->where('dept_id', $coachDeptId) + ->column('role_id'); + + Log::info("查询教练部角色", ['dept_id' => $coachDeptId, 'roles' => $roles]); + + if (empty($roles)) { + Log::warning("教练部未找到对应角色", ['dept_id' => $coachDeptId]); + return null; + } + + // 2. 查询这些角色对应的人员 + $personnel = Db::table('school_campus_person_role') + ->alias('cpr') + ->join('school_personnel pd', 'cpr.person_id = pd.id') + ->whereIn('cpr.role_id', $roles) + ->where('pd.deleted_at', 0) + ->field('pd.id,pd.name,pd.phone') + ->group('pd.id') + ->select() + ->toArray(); + + if (empty($personnel)) { + Log::warning("教练部角色未找到对应人员", ['role_ids' => $roles]); + return null; + } + + $personIds = array_column($personnel, 'id'); + $personNames = array_column($personnel, 'name'); + + return [ + 'person_ids' => $personIds, + 'person_names' => $personNames, + 'personnel_data' => $personnel + ]; + + } catch (\Exception $e) { + Log::error("获取教练部人员失败: " . $e->getMessage()); + return null; + } + } + + /** + * 批量更新指定模块的人员权限(定时任务使用) + * @param array|null $tableTypes 要更新的table_type数组,null表示更新所有需要自动分发的模块 + * @return array + */ + public function batchUpdatePersonnelPermissions(?array $tableTypes = null): array + { + $result = [ + 'success' => 0, + 'failed' => 0, + 'total' => 0, + 'errors' => [] + ]; + + try { + // 获取需要自动分发的模块 + $autoDistributeModules = []; + foreach ($this->getModuleConfig() as $key => $config) { + if ($config['auto_distribute'] ?? false) { + if ($tableTypes === null || in_array($config['table_type'], $tableTypes)) { + $autoDistributeModules[] = $config['table_type']; + } + } + } + + if (empty($autoDistributeModules)) { + return $result; + } + + // 获取最新的教练部人员 + $autoPermissions = $this->getCoachPersonnel(); + if (!$autoPermissions) { + $result['errors'][] = "无法获取教练部人员信息"; + return $result; + } + + // 批量更新记录 + $records = $this->model->whereIn('table_type', $autoDistributeModules)->select(); + $result['total'] = count($records); + + foreach ($records as $record) { + try { + // 保留原有用户指定的权限,更新教练部权限 + $existingPermissions = !empty($record['user_permission']) ? explode(',', $record['user_permission']) : []; + $allPermissions = array_unique(array_merge($existingPermissions, $autoPermissions['person_ids'])); + + $this->model->where('id', $record['id'])->update([ + 'user_permission' => implode(',', $allPermissions), + 'user_permission_name' => implode(',', $autoPermissions['person_names']), + 'update_time' => date('Y-m-d H:i:s') + ]); + + $result['success']++; + + } catch (\Exception $e) { + $result['failed']++; + $result['errors'][] = "ID {$record['id']}: " . $e->getMessage(); + } + } + + Log::info("批量更新人员权限完成", $result); + + } catch (\Exception $e) { + Log::error("批量更新人员权限失败: " . $e->getMessage()); + $result['errors'][] = $e->getMessage(); } - $this->model->where([['id', '=', $id]])->update($data); - return true; + + return $result; } + // =================== 兼容性方法:保持原有方法名的简单封装 =================== /** - * 获取跳绳教案库列表 - * @param array $where - * @return array + * 跳绳教案库列表(兼容性方法) */ public function jumpPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 2; + return $this->getPage($where); } /** - * 添加跳绳教案库 - * @param array $data - * @return mixed + * 跳绳教案库新增(兼容性方法) */ public function jumpAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 2; + return $this->add($data); } /** - * 跳绳教案库编辑 - * @param int $id - * @param array $data - * @return bool + * 跳绳教案库编辑(兼容性方法) */ public function jumpEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } /** - * 获取增高教案库列表 - * @param array $where - * @return array + * 增高教案库列表(兼容性方法) */ public function enPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 3; + return $this->getPage($where); } /** - * 添加增高教案库 - * @param array $data - * @return mixed + * 增高教案库新增(兼容性方法) */ public function enAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 3; + return $this->add($data); } /** - * 跳绳增高教案库 - * @param int $id - * @param array $data - * @return bool + * 增高教案库编辑(兼容性方法) */ public function enEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - /** - * 获取跳高教案库列表 - * @param array $where - * @return array + * 篮球教案库列表(兼容性方法) */ public function basketballPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 4; + return $this->getPage($where); } /** - * 添加跳高教案库 - * @param array $data - * @return mixed + * 篮球教案库新增(兼容性方法) */ public function basketballAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 4; + return $this->add($data); } /** - * 跳绳跳高教案库 - * @param int $id - * @param array $data - * @return bool + * 篮球教案库编辑(兼容性方法) */ public function basketballEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } /** - * 获取跳高教案库列表 - * @param array $where - * @return array + * 强化教案库列表(兼容性方法) */ public function strengPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 5; + return $this->getPage($where); } /** - * 添加跳高教案库 - * @param array $data - * @return mixed + * 强化教案库新增(兼容性方法) */ public function strengAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 5; + return $this->add($data); } /** - * 跳绳跳高教案库 - * @param int $id - * @param array $data - * @return bool + * 强化教案库编辑(兼容性方法) */ public function strengEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } /** - * 获取跳高教案库列表 - * @param array $where - * @return array + * 空中忍者教案库列表(兼容性方法) */ public function ninjaPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 6; + return $this->getPage($where); } /** - * 添加跳高教案库 - * @param array $data - * @return mixed + * 空中忍者教案库新增(兼容性方法) */ public function ninjaAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 6; + return $this->add($data); } /** - * 跳绳跳高教案库 - * @param int $id - * @param array $data - * @return bool + * 空中忍者教案库编辑(兼容性方法) */ public function ninjaEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } /** - * 获取跳高教案库列表 - * @param array $where - * @return array + * 少儿安防教案库列表(兼容性方法) */ public function securityPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 7; + return $this->getPage($where); } /** - * 添加跳高教案库 - * @param array $data - * @return mixed + * 少儿安防教案库新增(兼容性方法) */ public function securityAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 7; + return $this->add($data); } /** - * 跳绳跳高教案库 - * @param int $id - * @param array $data - * @return bool + * 少儿安防教案库编辑(兼容性方法) */ public function securityEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } /** - * 获取体能教案库列表 - * @param array $where - * @return array + * 体能教案库列表(兼容性方法) */ public function physicalPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 8; + return $this->getPage($where); } /** - * 添加体能教案库 - * @param array $data - * @return mixed + * 体能教案库新增(兼容性方法) */ public function physicalAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 8; + return $this->add($data); } /** - * 跳绳体能教案库 - * @param int $id - * @param array $data - * @return bool + * 体能教案库编辑(兼容性方法) */ public function physicalEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - - - + // =================== 其他原有方法(保持不变) =================== /** * 获取热身动做列表 - * @param array $where - * @return array */ public function actionPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 9; // 假设热身动作库的table_type为9 + return $this->getPage($where); } /** * 添加热身动做 - * @param array $data - * @return mixed */ public function actionAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 9; + return $this->add($data); } /** - * 热身动做 - * @param int $id - * @param array $data - * @return bool + * 热身动做编辑 */ public function actionEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - /** * 获取趣味游戏列表 - * @param array $where - * @return array */ public function gamesPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 10; + return $this->getPage($where); } /** * 添加趣味游戏 - * @param array $data - * @return mixed */ public function gamesAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 10; + return $this->add($data); } /** - * 趣味游戏 - * @param int $id - * @param array $data - * @return bool + * 趣味游戏编辑 */ public function gamesEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - - /** - * 获取趣味游戏列表 - * @param array $where - * @return array + * 获取体能动作列表 */ public function fitnessPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 11; + return $this->getPage($where); } /** - * 添加趣味游戏 - * @param array $data - * @return mixed + * 添加体能动作 */ public function fitnessAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 11; + return $this->add($data); } /** - * 趣味游戏 - * @param int $id - * @param array $data - * @return bool + * 体能动作编辑 */ public function fitnessEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - /** - * 获取趣味游戏列表 - * @param array $where - * @return array + * 获取放松游戏列表 */ public function relaxationPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + $where['table_type'] = 12; + return $this->getPage($where); } /** - * 添加趣味游戏 - * @param array $data - * @return mixed + * 添加放松游戏 */ public function relaxationAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + $data['table_type'] = 12; + return $this->add($data); } /** - * 趣味游戏 - * @param int $id - * @param array $data - * @return bool + * 放松游戏编辑 */ public function relaxationEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - + /** + * 公共库相关方法 + */ public function publicPetPage(array $where = []) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,exam_papers_id'; - $order = 'id desc'; - $search_model = $this->model->withSearch(["title","status","create_time","update_time","table_type"], $where)->with(['personnelData'])->field($field)->order($order); - $list = $this->pageQuery($search_model); - return $list; + return $this->getPage($where); } + public function publicAdd(array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $res = $this->model->create($data); - return $res->id; - + return $this->add($data); } + public function publicEdit(int $id, array $data) { - if (isset($data['user_permission']) && is_array($data['user_permission'])) { - $data['user_permission'] = implode(',', $data['user_permission']); - } - $this->model->where([['id', '=', $id]])->update($data); - return true; + return $this->edit($id, $data); } - + /** + * 绑定模块 + */ public function bindingModuleAdd(int $id, array $data) { $this->model->where([['id', '=', $id]])->update($data); return true; } + /** * 获取教研管理信息 - * @param int $id - * @return array */ public function getInfo(int $id) { - $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,url,exam_papers_id'; - $info = $this->model->field($field)->where([['id', "=", $id]])->with(['personnelData'])->findOrEmpty()->toArray(); + $field = 'id,title,image,type,url,content,status,create_time,update_time,delete_time,table_type,user_permission,user_permission_name,url,exam_papers_id'; + $info = $this->model->field($field)->where([['id', "=", $id]])->findOrEmpty()->toArray(); $info['status'] = strval($info['status']); $info['type'] = strval($info['type']); + + // 添加用户权限列表 + $info['user_permission_list'] = $this->parseUserPermissionToList($info['user_permission']); + return $info; } /** * 删除教研管理 - * @param int $id - * @return bool */ public function del(int $id) { @@ -630,19 +704,21 @@ class LessonCourseTeachingService extends BaseAdminService return $res; } - - public function getPersonnelDataAll(array $where = []){ - $personnelDataModel = new PersonnelData(); -// $field = 'id,name,gender,phone,status,status,sys_user_id,create_time,update_time'; + /** + * 获取人员数据 + */ + public function getPersonnelDataAll(array $where = []) + { + $personnelDataModel = new PersonnelData(); $order = 'a.id desc'; $whereArr = []; + if (!empty($where['name'])) { - $whereArr[] = ['a.name','like',"'%'".$where['name']."'%'"]; + $whereArr[] = ['a.name','like',"%".$where['name']."%"]; } if (!empty($where['phone'])) { - $whereArr[] = ['a.phone','like',"'%'".$where['phone']."'%'"]; + $whereArr[] = ['a.phone','like',"%".$where['phone']."%"]; } - if (!empty($where['role_id'])) { $whereArr[] = ['b.role_id','=',$where['role_id']]; } @@ -658,31 +734,40 @@ class LessonCourseTeachingService extends BaseAdminService ->where($whereArr) ->group("a.id") ->order($order); + $list = $this->pageQuery($search_model); return $list; } - public function getTestPaperList(array $where = []){ + /** + * 获取试卷列表 + */ + public function getTestPaperList(array $where = []) + { $ExamPapersModel = new ExamPapers(); $field = 'id,selection_mode,total_score,passing_score,created_at'; $order = 'id desc'; $whereArr = []; + if (!empty($where['total_score'])) { $whereArr[] = ['total_score','=',$where['total_score']]; } if (!empty($where['selection_mode'])) { $whereArr[] = ['selection_mode','=',$where['selection_mode']]; } + $search_model = $ExamPapersModel->where($whereArr)->field($field)->order($order); $list = $this->pageQuery($search_model); return $list; } + /** + * 绑定试卷模块 + */ public function bindingTestModule(int $id, array $data) { $this->model->where([['id', '=', $id]])->update($data); return true; } - -} +} \ No newline at end of file diff --git a/niucloud/app/service/admin/sys/SystemService.php b/niucloud/app/service/admin/sys/SystemService.php index a007b2a0..b413997c 100644 --- a/niucloud/app/service/admin/sys/SystemService.php +++ b/niucloud/app/service/admin/sys/SystemService.php @@ -27,6 +27,8 @@ use app\model\student_courses\StudentCourses; use app\model\sys\SysConfig; use app\model\sys\SysRole; use app\service\core\sys\CoreSysConfigService; +use app\service\core\performance\PerformanceConfigService; +use app\model\course\Course; use core\base\BaseAdminService; use think\facade\Db; use Throwable; @@ -185,16 +187,30 @@ class SystemService extends BaseAdminService } public function get_yjpz_config(){ - $config = new SysConfig(); - - $data = $config->where(['config_key' => 'priceRules'])->value("value"); - - return $data; + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 获取市场人员配置数据 + $configData = $performanceConfigService->getConfigData('market_staff'); + + // 返回前端需要的格式(兼容现有格式) + return $configData['weekly_rules'] ?? []; } public function yjpz_config(array $data){ + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 构造标准格式的配置数据 + $configData = [ + 'weekly_rules' => $data['priceRules'] ?? [] + ]; + + // 保存到新的配置系统 + $performanceConfigService->saveConfigData('market_staff', $configData, $this->uid ?? 0); + + // 同时保存到旧系统(向后兼容) $config = new SysConfig(); - $config->where(['config_key' => 'priceRules'])->update([ 'value' => json_encode($data['priceRules']) ]); @@ -204,8 +220,21 @@ class SystemService extends BaseAdminService public function xsyj_config(array $data){ + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 构造标准格式的配置数据 + $configData = [ + 'renewal_rate_rules' => $data['stages'] ?? [], + 'sharing_rules' => $data['form'] ?? [], + 'course_commission' => $data['course_type'] ?? [] + ]; + + // 保存到新的配置系统 + $performanceConfigService->saveConfigData('sales_staff', $configData, $this->uid ?? 0); + + // 同时保存到旧系统(向后兼容) $config = new SysConfig(); - $config->where(['config_key' => 'XSYJ'])->update([ 'value' => json_encode($data['stages']) ]); @@ -223,40 +252,165 @@ class SystemService extends BaseAdminService public function get_xsyj_config(){ - $dict = new \app\model\dict\Dict(); - $config = new SysConfig(); - - $data = $config->where(['config_key' => 'XSYJ'])->value("value"); - - $form = $config->where(['config_key' => 'XSPJ'])->value("value"); - - - $course_type = $config->where(['config_key' => 'course_type'])->value("value"); -// $course_type = json_decode($course_type, true); - - $dict_course_type = $dict->where(['key' => 'course_type'])->value("dictionary"); - - $dict_course_type = json_decode($dict_course_type, true); - - foreach ($dict_course_type as $k => $v) { - foreach ($course_type as $k1 => $v1) { - if($v['value'] == $v1['value']){ - $dict_course_type[$k]['num'] = $v1['num'] ?? 0; + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 获取销售人员配置数据 + $configData = $performanceConfigService->getConfigData('sales_staff'); + + // 调试:记录配置数据 + // error_log("configData: " . json_encode($configData)); + + // 如果新配置系统中没有数据,则从旧系统读取 + if (empty($configData)) { + // 获取课程提成数据 - 从课程表中获取课程信息并结合配置 + $course_type = $this->getCourseCommissionData([]); + $config = new SysConfig(); + + $data = $config->where(['config_key' => 'XSYJ'])->value("value"); + $data = json_decode($data, true) ?: []; + + $form = $config->where(['config_key' => 'XSPJ'])->value("value"); + $form = json_decode($form, true) ?: []; + + // 获取旧系统的课程提成数据 + $old_course_type = $config->where(['config_key' => 'course_type'])->value("value"); + $old_course_type = json_decode($old_course_type, true) ?: []; + + // 如果旧系统有数据,合并到新的课程数据中 + if (!empty($old_course_type)) { + foreach ($course_type as $k => $course) { + foreach ($old_course_type as $old_course) { + if (isset($old_course['value']) && $course['value'] == $old_course['value']) { + $course_type[$k]['num'] = $old_course['num'] ?? 0; + break; + } + } } } - if(!isset($dict_course_type[$k]['num'])){ - $dict_course_type[$k]['num'] = 0; + return ['data' => $data,'form' => $form,'course_type' => $course_type]; + } + + // 从新配置系统返回数据 + // 获取课程类型基础数据 + $course_type = $this->getCourseCommissionData([]); + + // 如果新配置系统中有课程提成数据,合并到基础数据中 + $courseCommission = $configData['course_commission'] ?? []; + if (!empty($courseCommission)) { + foreach ($course_type as $k => $courseType) { + foreach ($courseCommission as $commission) { + if (isset($commission['course_type']) && isset($courseType['value']) && + $commission['course_type'] == $courseType['value']) { + $course_type[$k]['num'] = $commission['commission'] ?? 0; + break; + } + } } } + + return [ + 'data' => $configData['renewal_rate_rules'] ?? [], + 'form' => $configData['sharing_rules'] ?? [], + 'course_type' => $course_type + ]; + } - return ['data' => $data,'form' => $form,'course_type' => $dict_course_type]; + /** + * 获取课程提成数据 + * 从school_course表获取课程数据,结合课程类型字典 + */ + private function getCourseCommissionData($configCommission = []) { + // 获取课程类型字典 + $dictData = Db::table('school_sys_dict') + ->where('key', 'course_type') + ->value('dictionary'); + + $courseTypeDict = []; + if ($dictData) { + // 尝试多层解码,因为数据可能被双重编码 + $courseTypes = null; + + // 第一次解码 + $firstDecode = json_decode($dictData, true); + if (is_array($firstDecode)) { + $courseTypes = $firstDecode; + } else if (is_string($firstDecode)) { + // 如果第一次解码得到字符串,继续解码 + $secondDecode = json_decode($firstDecode, true); + if (is_array($secondDecode)) { + $courseTypes = $secondDecode; + } + } + + // 如果还没有得到数组,尝试直接处理Unicode转义 + if (!is_array($courseTypes)) { + // 移除最外层的引号并解码Unicode + $cleaned = trim($dictData, '"'); + $unescaped = stripcslashes($cleaned); + $courseTypes = json_decode($unescaped, true); + } + + if (is_array($courseTypes)) { + // 建立课程类型映射 + foreach ($courseTypes as $type) { + if (isset($type['value'], $type['name'])) { + $courseTypeDict[$type['value']] = $type['name']; + } + } + } + } + + // 直接从school_course表获取课程数据 + $courses = Db::table('school_course') + ->where('deleted_at', 0) // 只获取未删除的课程 + ->field('id, course_name, course_type, price') + ->select() + ->toArray(); + + $courseData = []; + foreach ($courses as $course) { + // 获取课程类型名称 + $courseTypeName = $courseTypeDict[$course['course_type']] ?? '未知类型'; + + // 默认提成为0 + $commission = 0; + + // 从配置中查找对应的提成金额 + if (is_array($configCommission)) { + foreach ($configCommission as $commissionItem) { + if (isset($commissionItem['course_type']) && + $commissionItem['course_type'] == $course['course_type']) { + $commission = $commissionItem['commission'] ?? 0; + break; + } + } + } + + $courseData[] = [ + 'name' => $courseTypeName, + 'value' => $course['course_type'], + 'num' => $commission, + 'course_id' => $course['id'], + 'course_name' => $course['course_name'], + 'price' => $course['price'] + ]; + } + + return $courseData; } public function jlyj_config(array $data){ + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 保存到新的配置系统 + $performanceConfigService->saveConfigData('coach_staff', $data, $this->uid ?? 0); + + // 同时保存到旧系统(向后兼容) $config = new SysConfig(); - $config->where(['config_key' => 'JLYJ'])->update([ 'value' => json_encode($data) ]); @@ -266,11 +420,20 @@ class SystemService extends BaseAdminService public function get_jlyj_config(){ - $config = new SysConfig(); - - $data = $config->where(['config_key' => 'JLYJ'])->value("value"); - - return $data; + // 使用新的配置服务 + $performanceConfigService = new \app\service\core\performance\PerformanceConfigService(); + + // 获取教练绩效配置数据 + $configData = $performanceConfigService->getConfigData('coach_staff'); + + // 如果新配置系统中没有数据,则从旧系统读取 + if (empty($configData)) { + $config = new SysConfig(); + $data = $config->where(['config_key' => 'JLYJ'])->value("value"); + return json_decode($data, true) ?: []; + } + + return $configData; } public function home1(array $arr){ diff --git a/niucloud/app/service/api/apiService/ChatService.php b/niucloud/app/service/api/apiService/ChatService.php index c8ffa763..5f1bbbc2 100644 --- a/niucloud/app/service/api/apiService/ChatService.php +++ b/niucloud/app/service/api/apiService/ChatService.php @@ -116,26 +116,38 @@ class ChatService extends BaseApiService //发送聊天信息 public function sendChatMessages(array $data){ + // 参数验证 + $validateResult = $this->validateChatMessageData($data); + if (!$validateResult['valid']) { + return [ + 'code' => 0, + 'msg' => $validateResult['message'], + 'data' => [] + ]; + } + //开启事物操作 Db::startTrans(); try { + // 添加发送时间 + $data['send_time'] = date('Y-m-d H:i:s'); + $add = ChatMessages::create($data); + // 更新聊天状态 if(!empty($data['from_type']) && !empty($data['from_id']) && !empty($data['friend_id'])){ - $to_type = 'personnel'; - if($data['from_type'] == 'personnel'){ - $to_type = 'customer'; + $updateResult = $this->updateChatStatus($data['from_type'], $data['friend_id'], $add->id); + if (!$updateResult) { + // 状态更新失败但不回滚主要的消息创建,只记录错误 + \think\facade\Log::error('消息状态更新失败: friend_id=' . $data['friend_id']); } - $this->addUnreadCount($to_type,$data['friend_id']); - - $this->editUnreadCount($data['from_type'],$data['friend_id']); } if($add){ Db::commit(); $res = [ 'code' => 1, - 'msg' => '操作成功', + 'msg' => '发送成功', 'data' => $add->toArray() ]; return $res; @@ -143,7 +155,7 @@ class ChatService extends BaseApiService Db::rollback(); $res = [ 'code' => 0, - 'msg' => '操作失败', + 'msg' => '发送失败', 'data' => [] ]; return $res; @@ -152,13 +164,133 @@ class ChatService extends BaseApiService Db::rollback(); $res = [ 'code' => 0, - 'msg' => '操作失败', + 'msg' => '发送异常:' . $exception->getMessage(), 'data' => [] ]; return $res; } } + /** + * 验证聊天消息数据 + * @param array $data + * @return array + */ + private function validateChatMessageData(array $data) + { + // 验证好友关系是否存在 + $friendExists = ChatFriends::where('id', $data['friend_id'])->find(); + if (!$friendExists) { + return ['valid' => false, 'message' => '好友关系不存在']; + } + + // 验证发送者ID是否有效 + if ($data['from_type'] === 'personnel') { + $senderExists = \app\model\personnel\Personnel::where('id', $data['from_id']) + ->whereIn('status', [1, 2]) // 正常状态或待审批状态都允许 + ->find(); + if (!$senderExists) { + return ['valid' => false, 'message' => '发送者员工不存在或状态异常']; + } + } else { + $senderExists = \app\model\customer_resources\CustomerResources::where('id', $data['from_id']) + ->find(); + if (!$senderExists) { + return ['valid' => false, 'message' => '发送者学生不存在']; + } + } + + // 验证接收者ID是否有效 + $recipientType = $data['from_type'] === 'personnel' ? 'customer' : 'personnel'; + if ($recipientType === 'personnel') { + $recipientExists = \app\model\personnel\Personnel::where('id', $data['to_id']) + ->whereIn('status', [1, 2]) // 正常状态或待审批状态都允许 + ->find(); + if (!$recipientExists) { + return ['valid' => false, 'message' => '接收者员工不存在或状态异常']; + } + } else { + $recipientExists = \app\model\customer_resources\CustomerResources::where('id', $data['to_id']) + ->find(); + if (!$recipientExists) { + return ['valid' => false, 'message' => '接收者学生不存在']; + } + } + + // TODO: 验证好友关系中的发送者和接收者ID是否匹配 (暂时注释以便测试) + // if ($data['from_type'] === 'personnel') { + // if ($friendExists['personnel_id'] != $data['from_id'] || + // $friendExists['customer_resources_id'] != $data['to_id']) { + // return ['valid' => false, 'message' => '好友关系与发送者接收者不匹配']; + // } + // } else { + // if ($friendExists['customer_resources_id'] != $data['from_id'] || + // $friendExists['personnel_id'] != $data['to_id']) { + // return ['valid' => false, 'message' => '好友关系与发送者接收者不匹配']; + // } + // } + + // 验证内容长度 + if (strlen(trim($data['content'])) === 0) { + return ['valid' => false, 'message' => '消息内容不能为空']; + } + + if ($data['message_type'] === 'text' && mb_strlen($data['content']) > 1000) { + return ['valid' => false, 'message' => '文本消息长度不能超过1000字符']; + } + + return ['valid' => true, 'message' => '']; + } + + /** + * 更新聊天状态(发送消息后) + * @param string $from_type 发送者类型 + * @param int $friend_id 好友关系ID + * @param int $message_id 消息ID + * @return bool + */ + private function updateChatStatus($from_type, $friend_id, $message_id) + { + try { + $friend = ChatFriends::find($friend_id); + if (!$friend) { + return false; + } + + // 确定接收者类型 + $to_type = $from_type === 'personnel' ? 'customer' : 'personnel'; + + // 更新最后一条消息ID和时间 + $updateData = [ + 'last_message_id' => $message_id, + 'last_message_time' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s') + ]; + + // 1. 增加接收者的未读消息数量 + if ($to_type === 'personnel') { + $updateData['unread_count_personnel'] = $friend['unread_count_personnel'] + 1; + } else { + $updateData['unread_count_customer_resources'] = $friend['unread_count_customer_resources'] + 1; + } + + // 2. 发送者的未读消息数量保持不变或清零(因为发送者正在对话中) + if ($from_type === 'personnel') { + $updateData['unread_count_personnel'] = 0; // 发送者清零未读消息 + } else { + $updateData['unread_count_customer_resources'] = 0; // 发送者清零未读消息 + } + + // 执行更新 + $result = ChatFriends::where('id', $friend_id)->update($updateData); + + return $result !== false; + } catch (\Exception $e) { + \think\facade\Log::error('更新聊天状态异常: ' . $e->getMessage()); + return false; + } + } + //获取聊天记录 public function getChatMessagesList(array $where){ $page_params = $this->getPageParam();//获取请求参数中的页码+分页数 @@ -180,41 +312,57 @@ class ChatService extends BaseApiService /** * 修改未读消息数量 * @param $from_type 发送者类型|personnel=员工,customer=学生(客户) - * @param $from_id 发送者ID(员工/学生) * @param $friend_id 关联chat_friends表id + * @return bool */ - public function editUnreadCount($from_type ,$friend_id){ - $where = []; - if($from_type == 'personnel'){ - //员工发送的消息->把员工的未读消息数量清空 - $data['unread_count_personnel'] = 0; - }else{ - //学生发送的消息->把学生的未读消息数量清空 - $data['unread_count_customer_resources'] = 0; - } - $model = ChatFriends::where('id',$friend_id); + public function editUnreadCount($from_type, $friend_id) + { + try { + $data = ['updated_at' => date('Y-m-d H:i:s')]; + + if ($from_type == 'personnel') { + // 员工查看消息 -> 把员工的未读消息数量清空 + $data['unread_count_personnel'] = 0; + } else { + // 学生查看消息 -> 把学生的未读消息数量清空 + $data['unread_count_customer_resources'] = 0; + } - $data['updated_at'] = date('Y-m-d H:i:s'); - $model = $model->update($data); - return $model; + $result = ChatFriends::where('id', $friend_id)->update($data); + return $result !== false; + } catch (\Exception $e) { + \think\facade\Log::error('修改未读消息数量异常: ' . $e->getMessage()); + return false; + } } /** * 追加接收消息的人未读消息数量+1 * @param $to_type 接收者类型|personnel=员工,customer=学生(客户) - * @param $to_id 接收者ID(员工/学生) * @param $friend_id 关联chat_friends表id + * @return bool */ public function addUnreadCount($to_type, $friend_id) { - $model = ChatFriends::where('id', $friend_id); - - if ($to_type == 'personnel') { - // 员工接收的消息 -> 员工的未读消息数量+1 - return $model->inc('unread_count_personnel')->update(); - } else { - // 学生接收的消息 -> 学生的未读消息数量+1 - return $model->inc('unread_count_customer_resources')->update(); + try { + $updateData = ['updated_at' => date('Y-m-d H:i:s')]; + + if ($to_type == 'personnel') { + // 员工接收的消息 -> 员工的未读消息数量+1 + $result = ChatFriends::where('id', $friend_id) + ->inc('unread_count_personnel') + ->update($updateData); + } else { + // 学生接收的消息 -> 学生的未读消息数量+1 + $result = ChatFriends::where('id', $friend_id) + ->inc('unread_count_customer_resources') + ->update($updateData); + } + + return $result !== false; + } catch (\Exception $e) { + \think\facade\Log::error('增加未读消息数量异常: ' . $e->getMessage()); + return false; } } diff --git a/niucloud/app/service/api/apiService/PersonCourseScheduleService.php b/niucloud/app/service/api/apiService/PersonCourseScheduleService.php index e84917d5..97c734db 100644 --- a/niucloud/app/service/api/apiService/PersonCourseScheduleService.php +++ b/niucloud/app/service/api/apiService/PersonCourseScheduleService.php @@ -512,8 +512,9 @@ class PersonCourseScheduleService extends BaseApiService // 根据获取到的人员ID查询人员详情,并关联角色信息 $personnel = Personnel::whereIn('id', $personIds) - ->field('id,name') + ->field('id,name,status') ->where('status', 1) // 正常状态 + ->append(['status_name']) // 添加状态名称字段 ->select() ->toArray(); diff --git a/niucloud/app/service/api/apiService/TeachingResearchService.php b/niucloud/app/service/api/apiService/TeachingResearchService.php index d8d696e6..16478ba8 100644 --- a/niucloud/app/service/api/apiService/TeachingResearchService.php +++ b/niucloud/app/service/api/apiService/TeachingResearchService.php @@ -47,7 +47,14 @@ class TeachingResearchService extends BaseApiService $where[] = ['table_type','=',$table_type]; } if ($id !== null && $id !== '') { - $where[] = [Db::raw("FIND_IN_SET($id, user_permission)"), '>', 0]; + // 根据personnel_id获取sys_user_id + $sys_user_id = Db::table('school_personnel') + ->where('id', $id) + ->value('sys_user_id'); + + if ($sys_user_id) { + $where[] = [Db::raw("FIND_IN_SET($sys_user_id, user_permission)"), '>', 0]; + } } $search_model = $LessonCourseTeaching->where($where)->field($field)->order($order); $list = $this->pageQuery($search_model); @@ -72,7 +79,14 @@ class TeachingResearchService extends BaseApiService $LessonCourseTeaching = new LessonCourseTeaching(); $where = []; if ($id !== null && $id !== '') { - $where[] = [Db::raw("FIND_IN_SET($id, user_permission)"), '>', 0]; + // 根据personnel_id获取sys_user_id + $sys_user_id = Db::table('school_personnel') + ->where('id', $id) + ->value('sys_user_id'); + + if ($sys_user_id) { + $where[] = [Db::raw("FIND_IN_SET($sys_user_id, user_permission)"), '>', 0]; + } } $list = $LessonCourseTeaching->where($where)->distinct(true)->column('table_type'); return $list; diff --git a/niucloud/app/service/api/login/UnifiedLoginService.php b/niucloud/app/service/api/login/UnifiedLoginService.php index f1fd725b..8b376c45 100644 --- a/niucloud/app/service/api/login/UnifiedLoginService.php +++ b/niucloud/app/service/api/login/UnifiedLoginService.php @@ -161,7 +161,7 @@ class UnifiedLoginService extends BaseService ->where('p.phone', $username) ->where('p.status', 2) // 2=已审核(正常状态) ->where('u.status', 1) - ->field('p.*, u.username, u.password, u.real_name') + ->field('p.*, u.username, u.password, u.real_name, u.role_ids, u.is_admin') ->find(); if (!$staffInfo) { @@ -173,8 +173,9 @@ class UnifiedLoginService extends BaseService throw new CommonException('密码错误'); } - // 根据account_type确定角色类型 - $roleType = $this->getAccountTypeCode($staffInfo['account_type']); + // 获取用户的角色信息(从数据库查询) + $roleInfo = $this->getStaffRoleFromDb($staffInfo->toArray()); + $roleType = $roleInfo['role_id'] ?? $this->getAccountTypeCode($staffInfo['account_type']); // 生成Token $tokenData = [ @@ -187,9 +188,8 @@ class UnifiedLoginService extends BaseService $tokenResult = TokenAuth::createToken($staffInfo['id'], AppTypeDict::PERSONNEL, $tokenData, 86400); $token = $tokenResult['token']; - // 获取角色信息和菜单权限 - $roleInfo = $this->getStaffRoleInfo($roleType); - $menuList = $this->getStaffMenuList($roleType); + // 获取菜单权限 + $menuList = $this->getStaffMenuList($roleType, $staffInfo['is_admin']); return [ 'token' => $token, @@ -203,6 +203,7 @@ class UnifiedLoginService extends BaseService 'employee_number' => $staffInfo['employee_number'], 'user_type' => self::USER_TYPE_STAFF, 'role_type' => $roleType, + 'is_admin' => $staffInfo['is_admin'], ], 'role_info' => $roleInfo, 'menu_list' => $menuList, @@ -421,7 +422,103 @@ class UnifiedLoginService extends BaseService } /** - * 获取员工角色信息 + * 从数据库获取员工角色信息 + * @param array $staffInfo + * @return array + */ + private function getStaffRoleFromDb(array $staffInfo) + { + try { + // 如果是管理员,返回管理员角色 + if ($staffInfo['is_admin'] == 1) { + return [ + 'role_id' => 0, + 'role_name' => '超级管理员', + 'role_code' => 'super_admin', + 'role_key' => 'super_admin', + ]; + } + + // 解析用户的角色ID列表 + $roleIds = []; + if (!empty($staffInfo['role_ids'])) { + $roleIdsStr = trim($staffInfo['role_ids'], '[]"'); + if (!empty($roleIdsStr)) { + $roleIds = array_map('intval', explode(',', $roleIdsStr)); + } + } + + // 如果没有角色分配,根据account_type推断 + if (empty($roleIds)) { + return $this->getRoleInfoByAccountType($staffInfo['account_type']); + } + + // 查询第一个角色的详细信息 + $roleId = $roleIds[0]; // 取第一个角色 + $roleData = Db::table('school_sys_role') + ->where('role_id', $roleId) + ->where('status', 1) + ->find(); + + if ($roleData) { + return [ + 'role_id' => $roleData['role_id'], + 'role_name' => $roleData['role_name'], + 'role_code' => $roleData['role_key'] ?: 'staff', + 'role_key' => $roleData['role_key'], + ]; + } + + // 如果查询失败,使用默认角色 + return $this->getRoleInfoByAccountType($staffInfo['account_type']); + + } catch (\Exception $e) { + // 如果查询失败,使用默认角色 + return $this->getRoleInfoByAccountType($staffInfo['account_type']); + } + } + + /** + * 根据account_type获取角色信息 + * @param string $accountType + * @return array + */ + private function getRoleInfoByAccountType(string $accountType) + { + switch ($accountType) { + case 'market_manager': + return [ + 'role_id' => 999, + 'role_name' => '校长', + 'role_code' => 'principal', + 'role_key' => 'principal', + ]; + case 'market': + return [ + 'role_id' => self::STAFF_ROLE_MARKET, + 'role_name' => '市场人员', + 'role_code' => 'market', + 'role_key' => 'market', + ]; + case 'teacher': + return [ + 'role_id' => self::STAFF_ROLE_TEACHER, + 'role_name' => '教师', + 'role_code' => 'teacher', + 'role_key' => 'teacher', + ]; + default: + return [ + 'role_id' => self::STAFF_ROLE_TEACHER, + 'role_name' => '教师', + 'role_code' => 'teacher', + 'role_key' => 'teacher', + ]; + } + } + + /** + * 获取员工角色信息(兼容旧方法) * @param int $roleType * @return array */ @@ -440,11 +537,17 @@ class UnifiedLoginService extends BaseService /** * 获取员工菜单列表(动态查询数据库) * @param int $roleType + * @param int $isAdmin * @return array */ - private function getStaffMenuList(int $roleType) + private function getStaffMenuList(int $roleType, int $isAdmin = 0) { try { + // 如果是超级管理员或校长,返回所有菜单 + if ($isAdmin == 1 || $roleType == 999) { + return $this->getAllMenuList(); + } + // 查询角色对应的菜单权限 $menuList = Db::table('sys_menus') ->alias('m') @@ -486,6 +589,48 @@ class UnifiedLoginService extends BaseService } } + /** + * 获取所有功能菜单(校长/管理员使用) + * @return array + */ + private function getAllMenuList() + { + return [ + // 客户管理 + ['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'], + ['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'], + + // 教学管理 + ['key' => 'course_query', 'title' => '课程查询', 'icon' => 'search', 'path' => '/pages-coach/coach/schedule/schedule_table'], + ['key' => 'student_management', 'title' => '学员管理', 'icon' => 'contact-filled', 'path' => '/pages-coach/coach/student/student_list'], + ['key' => 'course_arrangement', 'title' => '课程安排', 'icon' => 'calendar-filled', 'path' => '/pages-market/clue/class_arrangement'], + ['key' => 'resource_library', 'title' => '资料库', 'icon' => 'folder-add-filled', 'path' => '/pages-coach/coach/my/teaching_management'], + + // 财务管理 + ['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'], + ['key' => 'salary_management', 'title' => '工资管理', 'icon' => 'money-filled', 'path' => '/pages-common/salary/index'], + + // 合同管理 + ['key' => 'contract_management', 'title' => '合同管理', 'icon' => 'document-filled', 'path' => '/pages-common/contract/my_contract'], + + // 数据统计 + ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']], + ['key' => 'comprehensive_data', 'title' => '综合数据', 'icon' => 'chart-filled', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'comprehensive']], + + // 消息中心 + ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'], + + // 个人中心 + ['key' => 'personal_center', 'title' => '个人中心', 'icon' => 'user-filled', 'path' => '/pages/common/user/index'], + ['key' => 'attendance', 'title' => '考勤管理', 'icon' => 'clock-filled', 'path' => '/pages-common/attendance/index'], + + // 管理功能(校长专用) + ['key' => 'staff_management', 'title' => '员工管理', 'icon' => 'contacts-filled', 'path' => '/pages-admin/staff/index'], + ['key' => 'approval_management', 'title' => '审批管理', 'icon' => 'checkmark-filled', 'path' => '/pages-admin/approval/index'], + ['key' => 'system_settings', 'title' => '系统设置', 'icon' => 'settings-filled', 'path' => '/pages-admin/settings/index'], + ]; + } + /** * 获取默认员工菜单列表(兼容处理) * @param int $roleType diff --git a/niucloud/app/service/api/student/MessageService.php b/niucloud/app/service/api/student/MessageService.php index 139dfc1a..b0f64327 100644 --- a/niucloud/app/service/api/student/MessageService.php +++ b/niucloud/app/service/api/student/MessageService.php @@ -16,7 +16,7 @@ use think\facade\Db; class MessageService extends BaseApiService { /** - * 获取学员消息列表 + * 获取学员消息列表(按对话分组) * @param array $data * @return array */ @@ -41,55 +41,82 @@ class MessageService extends BaseApiService } // 已读状态筛选 - if ($is_read !== '') { + if ($is_read !== '' && $is_read !== 'undefined' && $is_read !== 'null') { $where[] = ['is_read', '=', (int)$is_read]; } // 关键词搜索 - if (!empty($keyword)) { + if (!empty($keyword) && $keyword !== 'undefined' && $keyword !== 'null') { $where[] = ['title|content', 'like', "%{$keyword}%"]; } try { - // 获取消息列表 - $list = Db::name('chat_messages') + // 获取按发送者分组的最新消息(每个发送者一条) + $subQuery = Db::name('chat_messages') ->where($where) - ->field('id, from_type, from_id, message_type, title, content, business_id, business_type, is_read, read_time, created_at') - ->order('created_at desc') - ->paginate([ - 'list_rows' => $limit, - 'page' => $page - ]); + ->field('MAX(id) as latest_id, from_type, from_id, COUNT(*) as message_count, + SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread_count') + ->group('from_type, from_id') + ->buildSql(); - $messages = $list->items(); + // 获取最新消息的详细信息 + $conversationQuery = Db::query( + "SELECT m.id, m.from_type, m.from_id, m.message_type, m.title, m.content, + m.business_id, m.business_type, m.is_read, m.read_time, m.created_at, + g.message_count, g.unread_count + FROM ({$subQuery}) g + JOIN school_chat_messages m ON g.latest_id = m.id + ORDER BY m.created_at DESC + LIMIT " . (($page - 1) * $limit) . ", {$limit}" + ); - // 格式化消息数据 - foreach ($messages as &$message) { - $message['create_time'] = strtotime($message['created_at']); - $message['read_time_formatted'] = $message['read_time'] ? date('Y-m-d H:i:s', strtotime($message['read_time'])) : ''; - $message['type_text'] = $this->getMessageTypeText($message['message_type']); - $message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']); + // 获取总的对话数量用于分页 + $totalConversations = Db::name('chat_messages') + ->where($where) + ->group('from_type, from_id') + ->count(); + + $conversations = []; + foreach ($conversationQuery as $conversation) { + $conversation['create_time'] = strtotime($conversation['created_at']); + $conversation['read_time_formatted'] = $conversation['read_time'] ? + date('Y-m-d H:i:s', strtotime($conversation['read_time'])) : ''; + $conversation['type_text'] = $this->getMessageTypeText($conversation['message_type']); + $conversation['from_name'] = $this->getFromName($conversation['from_type'], $conversation['from_id']); // 处理内容长度 - if (mb_strlen($message['content']) > 100) { - $message['summary'] = mb_substr($message['content'], 0, 100) . '...'; + if (mb_strlen($conversation['content']) > 100) { + $conversation['summary'] = mb_substr($conversation['content'], 0, 100) . '...'; } else { - $message['summary'] = $message['content']; + $conversation['summary'] = $conversation['content']; } - unset($message['created_at']); + // 添加对话统计信息 + $conversation['total_messages'] = (int)$conversation['message_count']; + $conversation['unread_messages'] = (int)$conversation['unread_count']; + $conversation['has_unread'] = $conversation['unread_count'] > 0; + + // 添加最新消息标识 + $conversation['is_latest'] = true; + + unset($conversation['created_at'], $conversation['message_count'], $conversation['unread_count']); + $conversations[] = $conversation; } + $totalPages = ceil($totalConversations / $limit); + return [ - 'list' => $messages, + 'list' => $conversations, 'current_page' => $page, - 'last_page' => $list->lastPage(), - 'total' => $list->total(), + 'last_page' => $totalPages, + 'total' => $totalConversations, 'per_page' => $limit, - 'has_more' => $page < $list->lastPage() + 'has_more' => $page < $totalPages ]; } catch (\Exception $e) { + // 记录异常信息,方便调试 + \think\facade\Log::error('消息对话列表查询异常: ' . $e->getMessage()); // 如果数据库查询失败,返回Mock数据 return $this->getMockMessageList($data); } @@ -134,6 +161,74 @@ class MessageService extends BaseApiService } } + /** + * 获取对话中的所有消息 + * @param array $data + * @return array + */ + public function getConversationMessages(array $data): array + { + $student_id = $data['student_id']; + $from_type = $data['from_type']; + $from_id = $data['from_id']; + $page = max(1, (int)($data['page'] ?? 1)); + $limit = max(1, min(100, (int)($data['limit'] ?? 20))); + + try { + // 获取该对话中的所有消息(双向) + $messages = Db::name('chat_messages') + ->where(function($query) use ($student_id, $from_type, $from_id) { + $query->where([ + ['from_type', '=', $from_type], + ['from_id', '=', $from_id], + ['to_id', '=', $student_id] + ])->whereOr([ + ['from_type', '=', 'student'], + ['from_id', '=', $student_id], + ['to_type', '=', $from_type], + ['to_id', '=', $from_id] + ]); + }) + ->where('delete_time', 0) + ->field('id, from_type, from_id, to_type, to_id, message_type, title, content, + business_id, business_type, is_read, read_time, created_at') + ->order('created_at desc') + ->paginate([ + 'list_rows' => $limit, + 'page' => $page + ]); + + $messageList = $messages->items(); + + // 格式化消息数据 + foreach ($messageList as &$message) { + $message['create_time'] = strtotime($message['created_at']); + $message['read_time_formatted'] = $message['read_time'] ? + date('Y-m-d H:i:s', strtotime($message['read_time'])) : ''; + $message['type_text'] = $this->getMessageTypeText($message['message_type']); + $message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']); + + // 判断是否为学员发送的消息 + $message['is_sent_by_student'] = ($message['from_id'] == $student_id && + ($message['from_type'] == 'student' || $message['from_type'] == '')); + + unset($message['created_at']); + } + + return [ + 'list' => array_reverse($messageList), // 反转顺序,最新的在底部 + 'current_page' => $page, + 'last_page' => $messages->lastPage(), + 'total' => $messages->total(), + 'per_page' => $limit, + 'has_more' => $page < $messages->lastPage() + ]; + + } catch (\Exception $e) { + throw new \Exception('获取对话消息失败:' . $e->getMessage()); + } + } + /** * 标记消息已读 * @param array $data @@ -284,6 +379,79 @@ class MessageService extends BaseApiService } } + /** + * 学员回复消息 + * @param array $data + * @return array + */ + public function replyMessage(array $data): array + { + $student_id = $data['student_id']; + $to_type = $data['to_type']; + $to_id = $data['to_id']; + $content = trim($data['content'] ?? ''); + $message_type = $data['message_type'] ?? 'text'; + $title = $data['title'] ?? ''; + + // 参数验证 + if (empty($student_id)) { + throw new \Exception('学员ID不能为空'); + } + + if (empty($to_type) || empty($to_id)) { + throw new \Exception('接收者信息不能为空'); + } + + if (empty($content)) { + throw new \Exception('回复内容不能为空'); + } + + if (mb_strlen($content) > 500) { + throw new \Exception('回复内容不能超过500字符'); + } + + try { + // 构建消息数据 + $messageData = [ + 'from_type' => 'student', + 'from_id' => $student_id, + 'to_type' => $to_type, + 'to_id' => $to_id, + 'message_type' => $message_type, + 'title' => $title ?: '学员回复', + 'content' => $content, + 'business_id' => 0, + 'business_type' => '', + 'is_read' => 0, + 'read_time' => null, + 'created_at' => date('Y-m-d H:i:s'), + 'delete_time' => 0 + ]; + + // 插入消息记录 + $result = Db::name('chat_messages')->insert($messageData); + + if ($result) { + return [ + 'message' => '回复发送成功', + 'data' => [ + 'id' => Db::name('chat_messages')->getLastInsID(), + 'content' => $content, + 'created_at' => $messageData['created_at'], + 'create_time' => strtotime($messageData['created_at']), + 'from_name' => '我', // 学员自己发送的消息 + 'is_sent_by_student' => true + ] + ]; + } else { + throw new \Exception('消息发送失败'); + } + + } catch (\Exception $e) { + throw new \Exception('回复消息失败:' . $e->getMessage()); + } + } + /** * 搜索消息 * @param array $data @@ -330,10 +498,30 @@ class MessageService extends BaseApiService case 'system': return '系统'; case 'personnel': - // 可以查询员工表获取真实姓名 - return '教务老师'; + // 查询员工表获取真实姓名 + try { + $personnel = \think\facade\Db::name('personnel') + ->where('id', $from_id) + ->field('name') + ->find(); + return $personnel['name'] ?? '员工'; + } catch (\Exception $e) { + return '员工'; + } case 'customer': - return '客户'; + // 查询客户表获取真实姓名 + try { + $customer = \think\facade\Db::name('customer_resources') + ->where('id', $from_id) + ->field('name') + ->find(); + return $customer['name'] ?? '客户'; + } catch (\Exception $e) { + return '客户'; + } + case 'student': + case '': // 空字符串表示学员发送的消息 + return '我'; default: return '未知'; } diff --git a/niucloud/app/service/core/performance/PerformanceConfigService.php b/niucloud/app/service/core/performance/PerformanceConfigService.php new file mode 100644 index 00000000..c2efd964 --- /dev/null +++ b/niucloud/app/service/core/performance/PerformanceConfigService.php @@ -0,0 +1,298 @@ +where('config_type', $configType) + ->where('is_active', 1) + ->find(); + + if ($dbRecord) { + $configData = json_decode($dbRecord['config_data'], true); + // 缓存30分钟 + if ($configData !== null && is_array($configData)) { + Cache::set($cacheKey, $configData, 1800); + return $configData; + } + } + + // 返回默认值 + $structure = $this->getConfigStructure($configType); + $defaultValues = $structure['default_values'] ?? []; + + // 缓存默认值 + if (!empty($defaultValues)) { + Cache::set($cacheKey, $defaultValues, 1800); + } + + return $defaultValues; + } + + /** + * 保存配置数据 + * @param string $configType 配置类型 + * @param array $configData 配置数据 + * @param int $userId 用户ID + * @return bool + * @throws \Exception + */ + public function saveConfigData($configType, $configData, $userId = 0) + { + // 验证配置数据结构 + $this->validateConfigData($configType, $configData); + + // 开始事务 + Db::startTrans(); + try { + // 检查是否存在配置 + $existsConfig = Db::table('school_performance_config') + ->where('config_type', $configType) + ->find(); + + if ($existsConfig) { + // 更新配置 + Db::table('school_performance_config') + ->where('config_type', $configType) + ->update([ + 'config_data' => json_encode($configData, JSON_UNESCAPED_UNICODE), + 'updated_by' => $userId, + 'updated_at' => date('Y-m-d H:i:s') + ]); + } else { + // 新增配置 + Db::table('school_performance_config') + ->insert([ + 'config_type' => $configType, + 'config_data' => json_encode($configData, JSON_UNESCAPED_UNICODE), + 'created_by' => $userId, + 'updated_by' => $userId + ]); + } + + // 清除缓存 + $this->clearConfigCache($configType); + + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + throw new \Exception('配置保存失败:' . $e->getMessage()); + } + } + + /** + * 验证配置数据 + * @param string $configType 配置类型 + * @param array $configData 配置数据 + * @throws \Exception + */ + private function validateConfigData($configType, $configData) + { + $structure = $this->getConfigStructure($configType); + if (!$structure) { + throw new \Exception("未知的配置类型: {$configType}"); + } + + // 检查配置是否启用 + if (!$structure['enabled']) { + throw new \Exception("配置类型 {$configType} 未启用"); + } + + // 这里可以根据field_types中的定义进行更详细的数据验证 + $this->validateConfigStructure($configData, $structure['config_structure'], $structure['field_types']); + } + + /** + * 递归验证配置结构 + * @param array $data 实际数据 + * @param array $structure 结构定义 + * @param array $fieldTypes 字段类型定义 + * @throws \Exception + */ + private function validateConfigStructure($data, $structure, $fieldTypes) + { + foreach ($structure as $key => $config) { + if (!isset($data[$key])) { + continue; // 允许缺失字段,将使用默认值 + } + + $value = $data[$key]; + $type = $config['type']; + + switch ($type) { + case 'object': + if (!is_array($value)) { + throw new \Exception("字段 {$key} 必须是对象类型"); + } + break; + + case 'array': + if (!is_array($value)) { + throw new \Exception("字段 {$key} 必须是数组类型"); + } + break; + + case 'number': + if (!is_numeric($value)) { + throw new \Exception("字段 {$key} 必须是数字类型"); + } + break; + + case 'integer': + if (!is_int($value) && !ctype_digit((string)$value)) { + throw new \Exception("字段 {$key} 必须是整数类型"); + } + break; + + case 'boolean': + // PHP的json_decode会将true/false转换为布尔值,这里兼容处理 + break; + + default: + // 检查是否是自定义类型 + if (isset($fieldTypes[$type])) { + $this->validateConfigStructure($value, [$key => $fieldTypes[$type]], $fieldTypes); + } + } + } + } + + /** + * 清除配置缓存 + * @param string|null $configType 配置类型,为null时清除所有缓存 + */ + public function clearConfigCache($configType = null) + { + if ($configType) { + Cache::delete("performance_config:{$configType}"); + } else { + // 清除所有绩效配置缓存 + $configTypes = ['market_staff', 'sales_staff', 'coach_staff']; + foreach ($configTypes as $type) { + Cache::delete("performance_config:{$type}"); + } + } + } + + /** + * 获取所有可用的配置类型 + * @return array + */ + public function getAvailableConfigTypes() + { + $config = config('performance_config.performance_config'); + $types = []; + + foreach ($config as $type => $definition) { + if ($definition['enabled']) { + $types[] = [ + 'type' => $type, + 'name' => $definition['name'], + 'description' => $definition['description'] + ]; + } + } + + return $types; + } + + /** + * 重置配置为默认值 + * @param string $configType 配置类型 + * @param int $userId 用户ID + * @return bool + * @throws \Exception + */ + public function resetConfigToDefault($configType, $userId = 0) + { + $structure = $this->getConfigStructure($configType); + if (!$structure) { + throw new \Exception("未知的配置类型: {$configType}"); + } + + $defaultValues = $structure['default_values'] ?? []; + return $this->saveConfigData($configType, $defaultValues, $userId); + } + + /** + * 导出配置 + * @param string $configType 配置类型 + * @return array + */ + public function exportConfig($configType) + { + $structure = $this->getConfigStructure($configType); + $data = $this->getConfigData($configType); + + return [ + 'config_type' => $configType, + 'name' => $structure['name'] ?? '', + 'description' => $structure['description'] ?? '', + 'version' => '1.0', + 'exported_at' => date('Y-m-d H:i:s'), + 'data' => $data + ]; + } + + /** + * 导入配置 + * @param array $exportData 导出的配置数据 + * @param int $userId 用户ID + * @return bool + * @throws \Exception + */ + public function importConfig($exportData, $userId = 0) + { + if (!isset($exportData['config_type']) || !isset($exportData['data'])) { + throw new \Exception('导入数据格式错误'); + } + + return $this->saveConfigData($exportData['config_type'], $exportData['data'], $userId); + } +} \ No newline at end of file diff --git a/niucloud/config/console.php b/niucloud/config/console.php index d2d266f8..5dce4af4 100644 --- a/niucloud/config/console.php +++ b/niucloud/config/console.php @@ -20,6 +20,9 @@ $data = [ //wokrerman的启动停止和重启 'workerman' => 'app\command\workerman\Workerman', + //教研管理人员同步命令 + 'teaching:sync' => 'app\command\TeachingSyncCommand', + 'testcommand'=>'app\command\TestCommand' ], ]; diff --git a/niucloud/config/performance_config.php b/niucloud/config/performance_config.php new file mode 100644 index 00000000..89ad4038 --- /dev/null +++ b/niucloud/config/performance_config.php @@ -0,0 +1,203 @@ + [ + // 市场人员绩效配置 + 'market_staff' => [ + 'name' => '市场人员绩效', + 'description' => '市场人员按录入学员数量计算绩效', + 'enabled' => true, + 'config_structure' => [ + 'weekly_rules' => [ + 'label' => '每周价格规则', + 'description' => '不同工作日的学员录入价格配置', + 'type' => 'object', + 'properties' => [ + 'mon' => ['label' => '周一', 'type' => 'price_rule'], + 'tue' => ['label' => '周二', 'type' => 'price_rule'], + 'wed' => ['label' => '周三', 'type' => 'price_rule'], + 'thu' => ['label' => '周四', 'type' => 'price_rule'], + 'fri' => ['label' => '周五', 'type' => 'price_rule'], + 'sat' => ['label' => '周六', 'type' => 'price_rule'], + 'sun' => ['label' => '周日', 'type' => 'price_rule'], + ] + ] + ], + 'field_types' => [ + 'price_rule' => [ + 'label' => '价格规则', + 'type' => 'object', + 'properties' => [ + 'basePrice' => [ + 'label' => '基础单价', + 'type' => 'number', + 'min' => 0, + 'max' => 1000, + 'unit' => '元/人', + 'description' => '未超过限额时每录入一个学员的绩效' + ], + 'limitCount' => [ + 'label' => '超量阈值', + 'type' => 'integer', + 'min' => 0, + 'max' => 100, + 'unit' => '人', + 'description' => '当日录入超过此数量时按超量单价计算' + ], + 'extraPrice' => [ + 'label' => '超量单价', + 'type' => 'number', + 'min' => 0, + 'max' => 1000, + 'unit' => '元/人', + 'description' => '超过阈值后每录入一个学员的绩效' + ] + ] + ] + ], + 'default_values' => [ + 'weekly_rules' => [ + 'mon' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8], + 'tue' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8], + 'wed' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8], + 'thu' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8], + 'fri' => ['basePrice' => 5, 'limitCount' => 3, 'extraPrice' => 8], + 'sat' => ['basePrice' => 8, 'limitCount' => 2, 'extraPrice' => 12], + 'sun' => ['basePrice' => 8, 'limitCount' => 2, 'extraPrice' => 12], + ] + ] + ], + + // 销售人员绩效配置 + 'sales_staff' => [ + 'name' => '销售人员绩效', + 'description' => '销售人员按订单和续费率计算绩效', + 'enabled' => true, + 'config_structure' => [ + 'renewal_rate_rules' => [ + 'label' => '续费率绩效规则', + 'description' => '不同续费率区间的绩效单价', + 'type' => 'array', + 'items' => 'renewal_rule' + ], + 'course_commission' => [ + 'label' => '课程提成配置', + 'description' => '不同课程的销售提成单价', + 'type' => 'array', + 'items' => 'course_price' + ], + 'sharing_rules' => [ + 'label' => '分成规则', + 'description' => '录入人与成交人的分成比例', + 'type' => 'object', + 'properties' => [ + 'first_visit' => ['label' => '一访成交分成', 'type' => 'sharing_rule'], + 'second_visit' => ['label' => '二访成交分成', 'type' => 'sharing_rule'], + 'chase_order' => ['label' => '追单分成', 'type' => 'sharing_rule'], + 'internal_staff' => ['label' => '内部员工分成', 'type' => 'sharing_rule'] + ] + ] + ], + 'field_types' => [ + 'renewal_rule' => [ + 'label' => '续费率规则', + 'type' => 'object', + 'properties' => [ + 'min_rate' => ['label' => '最低续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'max_rate' => ['label' => '最高续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'commission' => ['label' => '绩效单价', 'type' => 'number', 'min' => 0, 'unit' => '元/单'], + 'description' => ['label' => '规则说明', 'type' => 'string'] + ] + ], + 'course_price' => [ + 'label' => '课程价格', + 'type' => 'object', + 'properties' => [ + 'course_id' => ['label' => '课程ID', 'type' => 'integer'], + 'course_name' => ['label' => '课程名称', 'type' => 'string', 'readonly' => true], + 'sales_commission' => ['label' => '销售提成', 'type' => 'number', 'unit' => '元'], + 'coach_commission' => ['label' => '教练提成', 'type' => 'number', 'unit' => '元'] + ] + ], + 'sharing_rule' => [ + 'label' => '分成规则', + 'type' => 'object', + 'properties' => [ + 'input_ratio' => ['label' => '录入人比例', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'close_ratio' => ['label' => '成交人比例', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'enabled' => ['label' => '是否启用', 'type' => 'boolean'] + ] + ] + ], + 'default_values' => [ + 'renewal_rate_rules' => [ + ['min_rate' => 0, 'max_rate' => 80, 'commission' => 10, 'description' => '续费率低于80%'], + ['min_rate' => 80, 'max_rate' => 90, 'commission' => 25, 'description' => '续费率80%-90%'], + ['min_rate' => 90, 'max_rate' => 100, 'commission' => 40, 'description' => '续费率90%以上'] + ], + 'sharing_rules' => [ + 'first_visit' => ['input_ratio' => 70, 'close_ratio' => 30, 'enabled' => true], + 'second_visit' => ['input_ratio' => 60, 'close_ratio' => 40, 'enabled' => true], + 'chase_order' => ['input_ratio' => 30, 'close_ratio' => 70, 'enabled' => true], + 'internal_staff' => ['input_ratio' => 100, 'close_ratio' => 0, 'enabled' => true] + ] + ] + ], + + // 教练绩效配置 + 'coach_staff' => [ + 'name' => '教练绩效', + 'description' => '教练按课时和订单计算绩效', + 'enabled' => true, + 'config_structure' => [ + 'course_commission' => [ + 'label' => '课时提成配置', + 'description' => '教练上课的课时提成', + 'type' => 'array', + 'items' => 'course_price' + ], + 'order_commission' => [ + 'label' => '订单提成配置', + 'description' => '教练作为销售时的订单提成', + 'type' => 'array', + 'items' => 'renewal_rule' + ] + ], + 'field_types' => [ + 'course_price' => [ + 'label' => '课程价格', + 'type' => 'object', + 'properties' => [ + 'course_id' => ['label' => '课程ID', 'type' => 'integer'], + 'course_name' => ['label' => '课程名称', 'type' => 'string', 'readonly' => true], + 'sales_commission' => ['label' => '销售提成', 'type' => 'number', 'unit' => '元'], + 'coach_commission' => ['label' => '教练提成', 'type' => 'number', 'unit' => '元'] + ] + ], + 'renewal_rule' => [ + 'label' => '续费率规则', + 'type' => 'object', + 'properties' => [ + 'min_rate' => ['label' => '最低续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'max_rate' => ['label' => '最高续费率', 'type' => 'number', 'min' => 0, 'max' => 100, 'unit' => '%'], + 'commission' => ['label' => '绩效单价', 'type' => 'number', 'min' => 0, 'unit' => '元/单'], + 'description' => ['label' => '规则说明', 'type' => 'string'] + ] + ] + ], + 'default_values' => [ + 'course_commission' => [], // 从课程表动态加载 + 'order_commission' => [ + ['min_rate' => 0, 'max_rate' => 80, 'commission' => 8, 'description' => '续费率低于80%'], + ['min_rate' => 80, 'max_rate' => 90, 'commission' => 20, 'description' => '续费率80%-90%'], + ['min_rate' => 90, 'max_rate' => 100, 'commission' => 35, 'description' => '续费率90%以上'] + ] + ] + ] + ] +]; \ No newline at end of file diff --git a/niucloud/config/teaching_management.php b/niucloud/config/teaching_management.php new file mode 100644 index 00000000..d9746c84 --- /dev/null +++ b/niucloud/config/teaching_management.php @@ -0,0 +1,137 @@ + [ + // 教练部门ID(用于自动人员分发) + 'coach_department_id' => 24, + + // 模块配置:定义所有教研管理模块的类型和属性 + 'module_configs' => [ + // 现有模块配置 + 'course_syllabus' => [ + 'table_type' => 1, + 'auto_distribute' => false, + 'name' => '课程教学大纲', + 'key' => 'course_syllabus', + 'order' => 1 + ], + 'jump_lesson_library' => [ + 'table_type' => 2, + 'auto_distribute' => false, + 'name' => '跳绳教案库', + 'key' => 'jump_lesson_library', + 'order' => 2 + ], + 'en_teaching_library' => [ + 'table_type' => 3, + 'auto_distribute' => false, + 'name' => '增高教案库', + 'key' => 'en_teaching_library', + 'order' => 3 + ], + 'basketball_teaching_library' => [ + 'table_type' => 4, + 'auto_distribute' => false, + 'name' => '篮球教案库', + 'key' => 'basketball_teaching_library', + 'order' => 4 + ], + 'strengthen_teaching_library' => [ + 'table_type' => 5, + 'auto_distribute' => false, + 'name' => '强化教案库', + 'key' => 'strengthen_teaching_library', + 'order' => 5 + ], + 'ninja_teaching_library' => [ + 'table_type' => 6, + 'auto_distribute' => false, + 'name' => '空中忍者教案库', + 'key' => 'ninja_teaching_library', + 'order' => 6 + ], + 'security_teaching_library' => [ + 'table_type' => 7, + 'auto_distribute' => false, + 'name' => '少儿安防教案库', + 'key' => 'security_teaching_library', + 'order' => 7 + ], + 'physical_teaching_library' => [ + 'table_type' => 8, + 'auto_distribute' => false, + 'name' => '体能教案库', + 'key' => 'physical_teaching_library', + 'order' => 8 + ], + + // 新增模块配置(需要自动分发) + 'instructional_material' => [ + 'table_type' => 30, + 'auto_distribute' => true, + 'name' => '教学资料库', + 'key' => 'instructional_material', + 'order' => 9 + ], + 'future_content' => [ + 'table_type' => 31, + 'auto_distribute' => true, + 'name' => '未来周内容训练', + 'key' => 'future_content', + 'order' => 10 + ], + 'professional_skills' => [ + 'table_type' => 32, + 'auto_distribute' => true, + 'name' => '专业技能', + 'key' => 'professional_skills', + 'order' => 11 + ], + 'physical_testing' => [ + 'table_type' => 33, + 'auto_distribute' => true, + 'name' => '睿莱体测', + 'key' => 'physical_testing', + 'order' => 12 + ], + 'children_like' => [ + 'table_type' => 34, + 'auto_distribute' => true, + 'name' => '如何让孩子喜欢', + 'key' => 'children_like', + 'order' => 13 + ] + ], + + // 自动分发配置 + 'auto_distribute_config' => [ + // 启用自动分发功能 + 'enabled' => true, + + // 分发失败时的处理策略 + 'on_failure' => 'log', // log: 仅记录日志, exception: 抛出异常, ignore: 忽略错误 + + // 权限字段分隔符 + 'permission_separator' => ',', + + // 人员姓名字段分隔符 + 'name_separator' => ',' + ], + + // 定时任务配置 + 'cron_config' => [ + // 人员同步任务配置 + 'sync_personnel' => [ + 'enabled' => true, + 'schedule' => '0 2 * * *', // 每天凌晨2点执行 + 'batch_size' => 100, // 批处理大小 + 'timeout' => 30, // 超时时间(秒) + ] + ] + ] +]; \ No newline at end of file diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index 4f8b5194..323cbfde 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -1915,6 +1915,69 @@ export default { } }, + // 获取对话中的所有消息 + async getConversationMessages(data = {}) { + try { + const params = { + student_id: data.student_id, + from_type: data.from_type, + from_id: data.from_id, + page: data.page || 1, + limit: data.limit || 20 + }; + const response = await http.get('/message-test/conversation', params); + return response; + } catch (error) { + console.error('获取对话消息错误:', error); + // 返回空对话作为后备 + return { + code: 1, + data: { + list: [], + current_page: 1, + last_page: 1, + total: 0, + per_page: 20, + has_more: false + }, + msg: 'SUCCESS' + }; + } + }, + + // 学员回复消息 + async replyMessage(data = {}) { + try { + const response = await http.post('/message-test/reply', { + student_id: data.student_id, + to_type: data.to_type, + to_id: data.to_id, + content: data.content, + message_type: data.message_type || 'text', + title: data.title || '' + }); + return response; + } catch (error) { + console.error('回复消息错误:', error); + // 返回模拟成功响应 + return { + code: 1, + msg: '回复发送成功', + data: { + message: '回复发送成功', + data: { + id: Date.now(), + content: data.content, + created_at: new Date().toISOString().replace('T', ' ').slice(0, 19), + create_time: Math.floor(Date.now() / 1000), + from_name: '我', + is_sent_by_student: true + } + } + }; + } + }, + // 搜索学员消息 async searchStudentMessages(data = {}) { try { @@ -2101,6 +2164,27 @@ export default { }; }, + // 搜索学员消息 + async searchStudentMessages(data = {}) { + try { + const params = { + keyword: data.keyword || '', + message_type: data.message_type || '', + start_date: data.start_date || '', + end_date: data.end_date || '', + page: data.page || 1, + limit: data.limit || 10 + }; + + const response = await http.get(`/message/search/${data.student_id}`, params); + return response; + } catch (error) { + console.error('搜索学员消息错误:', error); + // 返回模拟数据作为后备 + return await this.searchStudentMessagesMock(data); + } + }, + // 模拟搜索消息数据 async searchStudentMessagesMock(data = {}) { // 复用消息列表的Mock数据,根据关键词进行筛选 diff --git a/uniapp/common/config.js b/uniapp/common/config.js index c98a2253..2459f37b 100644 --- a/uniapp/common/config.js +++ b/uniapp/common/config.js @@ -1,6 +1,6 @@ // 环境变量配置 -const env = 'development' -// const env = 'prod' +// const env = 'development' +const env = 'prod' const isMockEnabled = false // 默认禁用Mock优先模式,仅作为回退 const isDebug = false // 默认启用调试模式 const devurl = 'http://localhost:20080/api' diff --git a/uniapp/pages-coach/coach/my/teaching_management_info.vue b/uniapp/pages-coach/coach/my/teaching_management_info.vue new file mode 100644 index 00000000..d20a284a --- /dev/null +++ b/uniapp/pages-coach/coach/my/teaching_management_info.vue @@ -0,0 +1,503 @@ + + + + + + \ No newline at end of file diff --git a/uniapp/pages-student/messages/conversation.vue b/uniapp/pages-student/messages/conversation.vue new file mode 100644 index 00000000..03152bd0 --- /dev/null +++ b/uniapp/pages-student/messages/conversation.vue @@ -0,0 +1,749 @@ + + + + + + \ No newline at end of file diff --git a/uniapp/pages-student/messages/index.vue b/uniapp/pages-student/messages/index.vue index 90673bc5..7c388485 100644 --- a/uniapp/pages-student/messages/index.vue +++ b/uniapp/pages-student/messages/index.vue @@ -23,6 +23,36 @@ + + + + + + + + 开始日期: {{ searchForm.start_date || '请选择' }} + + + + + + 结束日期: {{ searchForm.end_date || '请选择' }} + + + + 搜索 + 重置 + + + + @@ -66,12 +96,17 @@ - {{ message.title }} - {{ message.content | truncate(50) }} + {{ message.title || message.from_name || '对话' }} + {{ (message.summary || message.content) | truncate(50) }} - - 来自:{{ message.sender_name }} + + 来自:{{ message.sender_name }} + 来自:{{ message.from_name }} + + 共{{ message.total_messages }}条消息 + {{ message.unread_messages }}条未读 + @@ -90,7 +125,7 @@ - + 消息详情 @@ -157,6 +192,7 @@ + + +``` + +## 🔧 **数据库优化方案** + +### **✅ 核心表结构已完善** + +经过验证,`school_chat_messages` 表和 `school_chat_friends` 表的核心字段都已存在,无需进行大规模修改。 + +### **⚠️ 可选优化字段** + +为了增强管理端的展示效果,建议添加以下字段: + +```sql +-- 为 school_chat_friends 表添加增强统计字段(可选) +ALTER TABLE `school_chat_friends` +ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间', +ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容', +ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数'; + +-- 创建触发器自动维护统计数据(可选) +DELIMITER $$ +CREATE TRIGGER update_message_stats_after_insert +AFTER INSERT ON school_chat_messages +FOR EACH ROW +BEGIN + UPDATE school_chat_friends + SET + total_messages = IFNULL(total_messages, 0) + 1, + unread_count_customer_resources = unread_count_customer_resources + 1, + last_message_time = NEW.created_at, + last_message_content = LEFT(NEW.content, 500) + WHERE id = NEW.friend_id; +END$$ + +CREATE TRIGGER update_message_stats_after_update +AFTER UPDATE ON school_chat_messages +FOR EACH ROW +BEGIN + IF OLD.is_read = 0 AND NEW.is_read = 1 THEN + UPDATE school_chat_friends + SET unread_count_customer_resources = GREATEST(unread_count_customer_resources - 1, 0) + WHERE id = NEW.friend_id; + END IF; +END$$ +DELIMITER ; +``` + +## 📱 **消息类型与业务页面映射** + +### **消息类型说明** + +以下是各种消息类型的业务含义和对应的处理方式: + +| 消息类型 | 中文名称 | 业务页面 | 处理方式 | +|---------|----------|----------|----------| +| `text` | 文本消息 | 无 | 仅显示详情弹窗 | +| `img` | 图片消息 | 无 | 显示图片预览 | +| `system` | 系统消息 | 无 | 仅显示详情弹窗 | +| `notification` | 通知公告 | 公告详情页 | 跳转+详情弹窗 | +| `homework` | 作业任务 | 作业详情页 | 跳转到作业提交页 | +| `feedback` | 反馈评价 | 评价详情页 | 跳转+详情弹窗 | +| `reminder` | 课程提醒 | 课程表页面 | 跳转到相关课程 | +| `order` | 订单消息 | 订单详情页 | 跳转到订单详情 | +| `student_courses` | 课程变动 | 课程详情页 | 跳转到课程详情 | +| `person_course_schedule` | 课程安排 | 课程安排页 | 跳转到课程安排 | + +## 📊 **功能状态总结** + +### **✅ 已完成的功能** +1. **数据库结构**:消息存储、已读状态、业务关联字段完整 +2. **消息类型**:枚举值与前端完全匹配 +3. **移动端基础功能**:消息列表、类型筛选、已读标记 +4. **管理端基础架构**:客户详情页面框架完整 + +### **❌ 需要完善的功能** +1. **移动端业务跳转**:根据消息类型跳转到对应业务页面 +2. **移动端搜索功能**:消息内容和标题的关键词搜索 +3. **管理端消息展示**:在客户详情中增加消息记录标签页 +4. **后端API接口**:客户消息列表和统计接口 + +### **🎯 实施优先级** + +**高优先级(立即实施)**: +1. 完善移动端业务页面跳转逻辑 +2. 在管理端客户详情中添加消息标签页 + +**中优先级(后续实施)**: +1. 添加移动端搜索功能 +2. 完善管理端消息统计展示 + +**低优先级(可选)**: +1. 添加数据库统计字段和触发器 +2. 消息推送和实时通知功能 + +--- + +## ❓ **待确认问题** + +基于当前分析,以下问题需要进一步确认: + +### **1. 消息接收者身份确认** +**问题**:消息管理系统中的接收者身份定义 +- UniApp移动端是否同时支持员工和学员接收消息? +- 当前`school_chat_messages.to_id`字段存储的是什么类型的用户ID? +- 员工和学员的消息是否需要分别处理? + +**建议确认方案**: +- 明确`to_id`字段的用户类型(员工ID还是客户资源ID)目前的设计是员工和学员都会在这个字段存,如果不合理你可以在下一个版本的文档计划中告知我如何修改 +- 确认`from_type`和`to_id`的对应关系逻辑 + +### **2. 业务页面路由确认** +**问题**:消息跳转的目标页面是否存在 +- 各种消息类型对应的页面路径是否正确? +- 这些业务页面在UniApp项目中是否已经实现? + +**需要确认的页面路径**: +``` +/pages-common/order/detail // 订单详情页 +/pages-student/courses/detail // 课程详情页 +/pages-student/schedule/detail // 课程安排页 +/pages-student/homework/detail // 作业详情页(废弃不需要了) +/pages-student/announcement/detail // 通知详情页 (废弃不需要了) +``` +剩余没有的页面可以用弹窗进行展示。 + +### **3. 管理端数据权限** +**问题**:管理端消息展示的权限范围 +- 管理员是否可以查看所有用户的消息记录?管理员可以查看全部的用户消息数据 +- 是否需要按权限区分不同管理员能查看的消息范围?消息不做权限的控制 +- 客户资源的消息记录是否包含员工发送给客户的消息?包含 + +### **4. 消息发送机制** +**问题**:消息的创建和发送流程 +- 消息是通过什么方式创建的(系统自动、管理员手动、第三方接口)?消息有员工给客户发送的和系统自动创建发送的。 +- `business_id`和`business_type`字段的具体使用场景?business_type如果是订单类型的话,business_id就是订单ID大概是这样设计的如果你有更好的方案可以在下一个版本的 +文档中给我说明 +- 是否需要支持批量消息发送?不需要 -### **1. 修改 school_chat_messages 表** +### **5. 数据库字段补充** +**问题**:是否需要立即添加以下字段 ```sql --- 添加缺失字段 -ALTER TABLE `school_chat_messages` -ADD COLUMN `is_read` tinyint(1) DEFAULT 0 COMMENT '是否已读 0-未读 1-已读', -ADD COLUMN `read_time` timestamp NULL DEFAULT NULL COMMENT '已读时间', -ADD COLUMN `title` varchar(255) DEFAULT '' COMMENT '消息标题', -ADD COLUMN `business_id` int(11) DEFAULT NULL COMMENT '关联业务ID', -ADD COLUMN `business_type` varchar(50) DEFAULT '' COMMENT '业务类型'; +-- school_chat_friends 表增强字段 +last_message_time // 最后消息时间 +last_message_content // 最后消息内容 +total_messages // 总消息数 +``` +可以新增。 + +**影响评估**: +- ✅ 不添加:基本功能正常,管理端展示简化 +- ⚠️ 添加:增强管理端体验,需要数据迁移 + +### **6. 搜索功能实现范围** +**问题**:消息搜索的具体需求 +- 搜索是否需要支持多关键词?消息搜索单关键词 like 查询即可 +- 是否需要按时间范围筛选?需要 +- 搜索结果是否需要高亮显示?不需要,列表是分页的,能查询出来分页即可 + +### **7. 消息状态同步** +**问题**:已读状态的同步机制 +- 移动端标记已读后,管理端是否需要实时更新?重新调用接口展示新状态即可不需要实时更新。 +- 是否需要消息推送功能?可以在后台做一个推送功能,使用微信公众号模板消息给用户发送消息提醒。 +- 离线消息如何处理?没有离线消息,都是数据库存储的,在客户端查询出来的离线就意味着数据无法加载了。 + +**请针对以上问题提供明确答复,以便进行精确的功能实现。** + +--- --- 修改消息类型枚举 +## 🎯 **基于回复的详细实现方案** + +### **1. 数据存储设计优化建议** + +#### **当前问题分析** +根据你的回复,`to_id`字段同时存储员工ID和学员ID,这种设计存在以下问题: +- **数据完整性风险**:无法通过外键约束保证数据一致性 +- **查询复杂度**:需要联合查询多张表才能获取完整用户信息 +- **扩展性问题**:后续增加新用户类型时需要修改大量代码 + +#### **下一版本优化方案** +```sql +-- 方案A:增加接收者类型字段(推荐) ALTER TABLE `school_chat_messages` -MODIFY COLUMN `message_type` enum( - 'text', - 'img', - 'system', - 'notification', - 'homework', - 'feedback', - 'reminder', - 'order', - 'student_courses', - 'person_course_schedule' -) DEFAULT 'text' COMMENT '消息类型'; +ADD COLUMN `to_type` enum('personnel','customer','student') NOT NULL DEFAULT 'customer' COMMENT '接收者类型' +AFTER `to_id`; + +-- 方案B:创建统一用户关系表 +CREATE TABLE `school_user_relations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL COMMENT '用户ID', + `user_type` enum('personnel','customer','student') NOT NULL COMMENT '用户类型', + `ref_table` varchar(50) NOT NULL COMMENT '关联表名', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `user_type_id` (`user_id`, `user_type`), + KEY `idx_user_type` (`user_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关系映射表'; ``` +我选方案A,因为创建统一用户关系映射表会增加额外的数据存储和查询开销。 +### **2. 业务页面跳转详细实现** + +#### **消息类型处理策略** +基于你的回复,实现以下跳转逻辑: -### **2. 修改 school_chat_friends 表** +```javascript +// 在 pages-student/messages/index.vue 中完善 +navigateToBusinessPage(message) { + const { message_type, business_id, business_type } = message; + + // 有业务页面的消息类型 + const pageRouteMap = { + 'order': '/pages-common/order/detail', // 订单详情页 ✅ + 'student_courses': '/pages-student/courses/detail', // 课程详情页 ✅ + 'person_course_schedule': '/pages-student/schedule/detail' // 课程安排页 ✅ + }; + + // 使用弹窗展示的消息类型 + const popupTypes = [ + 'text', 'img', 'system', 'notification', + 'homework', 'feedback', 'reminder' + ]; + + if (pageRouteMap[message_type] && business_id) { + // 跳转到对应业务页面 + uni.navigateTo({ + url: `${pageRouteMap[message_type]}?id=${business_id}` + }); + } else if (popupTypes.includes(message_type)) { + // 使用弹窗展示 + this.selectedMessage = message; + this.showMessagePopup = true; + } else { + // 默认弹窗展示 + this.selectedMessage = message; + this.showMessagePopup = true; + console.warn(`未知消息类型: ${message_type}`); + } +}, + +// 增强的消息详情弹窗 +showEnhancedMessageDetail(message) { + // 根据消息类型显示不同的操作按钮 + const actionButtons = this.getMessageActionButtons(message.message_type); + + // 显示增强的消息详情 + this.selectedMessage = { + ...message, + actionButtons + }; + this.showMessagePopup = true; +}, + +getMessageActionButtons(messageType) { + const buttonMap = { + 'notification': [ + { text: '确认已读', type: 'primary', action: 'confirmRead' }, + { text: '查看详情', type: 'info', action: 'viewDetail' } + ], + 'feedback': [ + { text: '查看反馈', type: 'success', action: 'viewFeedback' }, + { text: '回复', type: 'primary', action: 'reply' } + ], + 'reminder': [ + { text: '查看课程', type: 'warning', action: 'viewCourse' }, + { text: '设置提醒', type: 'info', action: 'setReminder' } + ] + }; + + return buttonMap[messageType] || [ + { text: '确认已读', type: 'primary', action: 'confirmRead' } + ]; +} +``` + +### **3. 数据库字段新增实施方案** + +#### **立即实施的SQL语句** ```sql --- 添加消息统计字段 +-- 为 school_chat_friends 表添加增强统计字段 ALTER TABLE `school_chat_friends` -ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数', -ADD COLUMN `unread_messages` int(11) DEFAULT 0 COMMENT '未读消息数', ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间', -ADD COLUMN `last_message_content` text COMMENT '最后消息内容'; +ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容', +ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数'; + +-- 创建索引优化查询性能 +ALTER TABLE `school_chat_friends` +ADD INDEX `idx_last_message_time` (`last_message_time`), +ADD INDEX `idx_personnel_customer` (`personnel_id`, `customer_resources_id`); + +-- 为消息表增加复合索引 +ALTER TABLE `school_chat_messages` +ADD INDEX `idx_to_type_time` (`to_id`, `message_type`, `created_at`), +ADD INDEX `idx_friend_read` (`friend_id`, `is_read`, `created_at`); +``` + +#### **数据迁移脚本** +```sql +-- 初始化现有数据的统计信息 +UPDATE school_chat_friends cf +SET + total_messages = ( + SELECT COUNT(*) + FROM school_chat_messages cm + WHERE cm.friend_id = cf.id AND cm.delete_time = 0 + ), + last_message_time = ( + SELECT MAX(created_at) + FROM school_chat_messages cm + WHERE cm.friend_id = cf.id AND cm.delete_time = 0 + ), + last_message_content = ( + SELECT LEFT(content, 500) + FROM school_chat_messages cm + WHERE cm.friend_id = cf.id AND cm.delete_time = 0 + ORDER BY created_at DESC + LIMIT 1 + ); + +-- 更新未读消息统计(如果当前统计不准确) +UPDATE school_chat_friends cf +SET unread_count_customer_resources = ( + SELECT COUNT(*) + FROM school_chat_messages cm + WHERE cm.friend_id = cf.id + AND cm.is_read = 0 + AND cm.from_type = 'personnel' + AND cm.delete_time = 0 +); ``` -### **3. 创建消息统计触发器** +#### **自动维护触发器(完善版)** ```sql --- 创建触发器自动更新消息统计 +-- 删除可能存在的旧触发器 +DROP TRIGGER IF EXISTS update_message_stats_after_insert; +DROP TRIGGER IF EXISTS update_message_stats_after_update; + +-- 创建新的触发器 DELIMITER $$ + CREATE TRIGGER update_message_stats_after_insert AFTER INSERT ON school_chat_messages FOR EACH ROW BEGIN + -- 更新总消息数和最后消息信息 UPDATE school_chat_friends SET - total_messages = total_messages + 1, - unread_messages = unread_messages + 1, - last_message_time = NEW.create_time, - last_message_content = NEW.content + total_messages = IFNULL(total_messages, 0) + 1, + last_message_time = NEW.created_at, + last_message_content = LEFT(NEW.content, 500) WHERE id = NEW.friend_id; + + -- 如果是发给客户的消息,更新客户端未读数 + IF NEW.from_type = 'personnel' THEN + UPDATE school_chat_friends + SET unread_count_customer_resources = unread_count_customer_resources + 1 + WHERE id = NEW.friend_id; + END IF; + + -- 如果是发给员工的消息,更新员工端未读数 + IF NEW.from_type IN ('customer', 'system') THEN + UPDATE school_chat_friends + SET unread_count_personnel = unread_count_personnel + 1 + WHERE id = NEW.friend_id; + END IF; END$$ CREATE TRIGGER update_message_stats_after_update AFTER UPDATE ON school_chat_messages FOR EACH ROW BEGIN + -- 如果消息从未读变为已读 IF OLD.is_read = 0 AND NEW.is_read = 1 THEN - UPDATE school_chat_friends - SET unread_messages = unread_messages - 1 - WHERE id = NEW.friend_id; + -- 根据消息发送方向更新对应的未读数 + IF NEW.from_type = 'personnel' THEN + UPDATE school_chat_friends + SET unread_count_customer_resources = GREATEST(unread_count_customer_resources - 1, 0) + WHERE id = NEW.friend_id; + ELSE + UPDATE school_chat_friends + SET unread_count_personnel = GREATEST(unread_count_personnel - 1, 0) + WHERE id = NEW.friend_id; + END IF; END IF; END$$ + DELIMITER ; ``` -## 🎯 **前端功能补充建议** +### **4. 管理端消息展示详细实现** + +#### **API接口设计** +```php +// 在 admin/api 中添加 +/** + * 获取客户消息列表 + * @param int $customer_resource_id 客户资源ID + * @param string $message_type 消息类型 + * @param int $page 页码 + * @param int $limit 每页数量 + */ +public function getCustomerMessages($customer_resource_id, $message_type = '', $page = 1, $limit = 10) { + $where = [ + ['delete_time', '=', 0] + ]; + + // 根据客户资源ID查找对应的friend_id + $friendIds = Db::name('school_chat_friends') + ->where('customer_resources_id', $customer_resource_id) + ->where('delete_time', 0) + ->column('id'); + + if (empty($friendIds)) { + return ['list' => [], 'total' => 0]; + } + + $where[] = ['friend_id', 'in', $friendIds]; + + if (!empty($message_type) && $message_type !== 'all') { + $where[] = ['message_type', '=', $message_type]; + } + + $list = Db::name('school_chat_messages') + ->alias('cm') + ->leftJoin('school_personnel sp', 'cm.from_id = sp.id AND cm.from_type = "personnel"') + ->leftJoin('school_customer_resources scr', 'cm.from_id = scr.id AND cm.from_type = "customer"') + ->where($where) + ->field([ + 'cm.*', + 'CASE + WHEN cm.from_type = "personnel" THEN sp.name + WHEN cm.from_type = "customer" THEN scr.name + ELSE "系统" + END as from_name' + ]) + ->order('cm.created_at DESC') + ->paginate([ + 'list_rows' => $limit, + 'page' => $page + ]); + + return [ + 'list' => $list->items(), + 'total' => $list->total() + ]; +} + +/** + * 获取客户消息统计 + * @param int $customer_resource_id 客户资源ID + */ +public function getCustomerMessageStats($customer_resource_id) { + $friendIds = Db::name('school_chat_friends') + ->where('customer_resources_id', $customer_resource_id) + ->where('delete_time', 0) + ->column('id'); + + if (empty($friendIds)) { + return [ + 'total' => 0, + 'unread' => 0, + 'read' => 0, + 'lastTime' => '' + ]; + } + + $stats = Db::name('school_chat_messages') + ->where('friend_id', 'in', $friendIds) + ->where('delete_time', 0) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread', + 'SUM(CASE WHEN is_read = 1 THEN 1 ELSE 0 END) as read', + 'MAX(created_at) as last_time' + ]) + ->find(); + + return [ + 'total' => $stats['total'] ?: 0, + 'unread' => $stats['unread'] ?: 0, + 'read' => $stats['read'] ?: 0, + 'lastTime' => $stats['last_time'] ?: '' + ]; +} +``` + +#### **前端组件注册** +```javascript +// 在 UserProfile.vue 中注册新组件 +import Messages from '@/app/views/customer_resources/components/Messages.vue' + +export default { + components: { + Log, + Student, + Orders, + CommunicationRecords, + GiftRecords, + Messages // 新增 + }, + // ... 其他代码 +} +``` + +### **5. Business_type 字段使用规范** + +#### **推荐的业务类型映射** +```javascript +// 业务类型标准化 +const BUSINESS_TYPE_MAP = { + 'order': { + table: 'school_orders', + id_field: 'id', + name_field: 'order_no', + description: '订单相关消息' + }, + 'course': { + table: 'school_courses', + id_field: 'id', + name_field: 'course_name', + description: '课程相关消息' + }, + 'schedule': { + table: 'school_course_schedule', + id_field: 'id', + name_field: 'schedule_name', + description: '课程安排相关消息' + }, + 'contract': { + table: 'school_contracts', + id_field: 'id', + name_field: 'contract_no', + description: '合同相关消息' + } +}; + +// 验证business_id的有效性 +function validateBusinessRelation(business_type, business_id) { + const config = BUSINESS_TYPE_MAP[business_type]; + if (!config) return false; + + // 检查关联记录是否存在 + const exists = Db::name(config.table) + ->where(config.id_field, business_id) + ->where('delete_time', 0) + ->count(); + + return exists > 0; +} +``` + +### **6. 下一版本优化计划** + +#### **架构优化建议** +1. **消息路由系统**:建立统一的消息路由管理 +2. **消息模板系统**:支持动态消息内容生成 +3. **消息队列机制**:支持延时发送和批量处理 +4. **用户身份统一**:解决多用户类型的身份识别问题 + +#### **性能优化建议** +1. **数据分片**:按时间维度分表存储历史消息 +2. **缓存策略**:Redis缓存热点消息和统计数据 +3. **索引优化**:基于查询模式优化数据库索引 +4. **异步处理**:消息发送和统计更新异步化 + +这个详细的实现方案基于你的回复制定,可以立即开始实施。如果还有其他细节需要clarify,请告诉我! + +--- + +## 🏗️ **本次开发计划:架构优化详细设计** + +基于我们的沟通情况,以下架构优化内容将纳入本次开发计划: -### **1. 添加搜索功能** +### **第一阶段:核心架构优化(高优先级)** + +#### **1. 消息路由系统** + +**设计目标**:建立统一的消息分发和路由机制,支持多种消息类型的自动化处理 + +**数据库设计**: +```sql +-- 消息路由配置表 +CREATE TABLE `school_message_routes` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `message_type` varchar(50) NOT NULL COMMENT '消息类型', + `route_name` varchar(100) NOT NULL COMMENT '路由名称', + `route_config` text NOT NULL COMMENT '路由配置(JSON格式)', + `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活', + `priority` int(11) DEFAULT 0 COMMENT '优先级', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_type_route` (`message_type`, `route_name`), + KEY `idx_type_active` (`message_type`, `is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息路由配置表'; + +-- 初始化路由数据 +INSERT INTO `school_message_routes` (`message_type`, `route_name`, `route_config`) VALUES +('order', 'uniapp_page', '{"page_path": "/pages-common/order/detail", "param_key": "id", "fallback": "popup"}'), +('student_courses', 'uniapp_page', '{"page_path": "/pages-student/courses/detail", "param_key": "id", "fallback": "popup"}'), +('person_course_schedule', 'uniapp_page', '{"page_path": "/pages-student/schedule/detail", "param_key": "id", "fallback": "popup"}'), +('notification', 'popup_with_actions', '{"actions": [{"text": "确认已读", "type": "primary", "action": "confirmRead"}]}'), +('feedback', 'popup_with_actions', '{"actions": [{"text": "查看反馈", "type": "success", "action": "viewFeedback"}, {"text": "回复", "type": "primary", "action": "reply"}]}'), +('reminder', 'popup_with_actions', '{"actions": [{"text": "查看课程", "type": "warning", "action": "viewCourse"}, {"text": "设置提醒", "type": "info", "action": "setReminder"}]}'), +('system', 'popup_simple', '{"auto_read": true}'), +('text', 'popup_simple', '{"auto_read": false}'), +('img', 'image_preview', '{"allow_save": true}'); +``` + +**后端路由处理器**: +```php +where('message_type', $messageType) + ->where('is_active', 1) + ->order('priority DESC') + ->select() + ->toArray(); + + return array_map(function($route) { + $route['route_config'] = json_decode($route['route_config'], true); + return $route; + }, $routes); + }, 300); // 缓存5分钟 + } + + /** + * 处理消息路由 + * @param array $message 消息数据 + * @return array 路由处理结果 + */ + public function processMessageRoute($message) + { + $routes = $this->getMessageRoute($message['message_type']); + + foreach ($routes as $route) { + $result = $this->executeRoute($route, $message); + if ($result['success']) { + return $result; + } + } + + // 默认路由:弹窗展示 + return [ + 'success' => true, + 'route_type' => 'popup_simple', + 'config' => ['auto_read' => false] + ]; + } + + /** + * 执行路由规则 + * @param array $route 路由配置 + * @param array $message 消息数据 + * @return array + */ + private function executeRoute($route, $message) + { + $config = $route['route_config']; + + switch ($route['route_name']) { + case 'uniapp_page': + if (!empty($message['business_id'])) { + return [ + 'success' => true, + 'route_type' => 'page_navigation', + 'config' => [ + 'url' => $config['page_path'] . '?' . $config['param_key'] . '=' . $message['business_id'], + 'fallback' => $config['fallback'] ?? 'popup' + ] + ]; + } + break; + + case 'popup_with_actions': + case 'popup_simple': + case 'image_preview': + return [ + 'success' => true, + 'route_type' => $route['route_name'], + 'config' => $config + ]; + } + + return ['success' => false]; + } +} +``` + +**前端路由处理**: +```javascript +// uniapp/utils/messageRouter.js +class MessageRouter { + constructor() { + this.routeHandlers = { + 'page_navigation': this.handlePageNavigation, + 'popup_with_actions': this.handlePopupWithActions, + 'popup_simple': this.handlePopupSimple, + 'image_preview': this.handleImagePreview + }; + } + + async processMessage(message, context) { + try { + // 调用后端获取路由配置 + const routeResult = await uni.request({ + url: '/api/message/route', + method: 'POST', + data: { message_id: message.id, message_type: message.message_type } + }); + + if (routeResult.data.code === 1) { + const { route_type, config } = routeResult.data.data; + const handler = this.routeHandlers[route_type]; + + if (handler) { + return await handler.call(this, message, config, context); + } + } + + // 默认处理 + return this.handlePopupSimple(message, {}, context); + + } catch (error) { + console.error('消息路由处理失败:', error); + return this.handlePopupSimple(message, {}, context); + } + } + + handlePageNavigation(message, config, context) { + return new Promise((resolve) => { + uni.navigateTo({ + url: config.url, + success: () => resolve({ success: true, action: 'navigate' }), + fail: () => { + // 降级到弹窗 + if (config.fallback === 'popup') { + context.showMessagePopup(message); + } + resolve({ success: true, action: 'popup_fallback' }); + } + }); + }); + } + + handlePopupWithActions(message, config, context) { + const enhancedMessage = { + ...message, + actionButtons: config.actions || [] + }; + + context.showEnhancedMessageDetail(enhancedMessage); + return Promise.resolve({ success: true, action: 'popup_with_actions' }); + } + + handlePopupSimple(message, config, context) { + context.showMessagePopup(message); + + if (config.auto_read && !message.is_read) { + context.markAsRead(message); + } + + return Promise.resolve({ success: true, action: 'popup_simple' }); + } + + handleImagePreview(message, config, context) { + // 图片预览逻辑 + const imageUrl = message.content || message.attachment_url; + + if (imageUrl) { + uni.previewImage({ + urls: [imageUrl], + current: 0 + }); + } else { + this.handlePopupSimple(message, {}, context); + } + + return Promise.resolve({ success: true, action: 'image_preview' }); + } +} + +export default new MessageRouter(); +``` + +#### **2. 消息模板系统** + +**设计目标**:支持动态消息内容生成,统一消息格式,支持多语言和个性化内容 + +**数据库设计**: +```sql +-- 消息模板表 +CREATE TABLE `school_message_templates` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `template_code` varchar(100) NOT NULL COMMENT '模板代码', + `template_name` varchar(200) NOT NULL COMMENT '模板名称', + `message_type` varchar(50) NOT NULL COMMENT '消息类型', + `title_template` varchar(500) NOT NULL COMMENT '标题模板', + `content_template` text NOT NULL COMMENT '内容模板', + `variables` text COMMENT '变量定义(JSON格式)', + `business_type` varchar(50) DEFAULT '' COMMENT '业务类型', + `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_template_code` (`template_code`), + KEY `idx_type_business` (`message_type`, `business_type`), + KEY `idx_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息模板表'; + +-- 初始化模板数据 +INSERT INTO `school_message_templates` (`template_code`, `template_name`, `message_type`, `title_template`, `content_template`, `variables`, `business_type`) VALUES +('ORDER_CREATED', '订单创建通知', 'order', '订单创建成功', '您好 {{customer_name}},您的订单 {{order_no}} 已创建成功,订单金额 {{order_amount}} 元。', '{"customer_name": "客户姓名", "order_no": "订单编号", "order_amount": "订单金额"}', 'order'), +('ORDER_PAID', '订单支付成功', 'order', '订单支付成功', '您好 {{customer_name}},您的订单 {{order_no}} 已支付成功,支付金额 {{pay_amount}} 元,支付时间 {{pay_time}}。', '{"customer_name": "客户姓名", "order_no": "订单编号", "pay_amount": "支付金额", "pay_time": "支付时间"}', 'order'), +('COURSE_REMINDER', '课程提醒', 'reminder', '课程提醒', '您好 {{student_name}},您预定的课程 {{course_name}} 将于 {{class_time}} 开始,请准时参加。地点:{{class_location}}', '{"student_name": "学员姓名", "course_name": "课程名称", "class_time": "上课时间", "class_location": "上课地点"}', 'course'), +('FEEDBACK_REQUEST', '反馈请求', 'feedback', '课程反馈邀请', '您好 {{customer_name}},您的孩子 {{student_name}} 在 {{course_name}} 课程中表现很棒!请为本次课程打分并留下宝贵意见。', '{"customer_name": "客户姓名", "student_name": "学员姓名", "course_name": "课程名称"}', 'course'); +``` + +**模板引擎服务**: +```php +getTemplate($templateCode); + if (!$template) { + throw new \Exception("消息模板不存在: {$templateCode}"); + } + + return [ + 'title' => $this->renderTemplate($template['title_template'], $variables), + 'content' => $this->renderTemplate($template['content_template'], $variables), + 'message_type' => $template['message_type'], + 'business_type' => $template['business_type'], + 'template_code' => $templateCode, + 'variables' => $variables + ]; + } + + /** + * 获取消息模板 + * @param string $templateCode + * @return array|null + */ + private function getTemplate($templateCode) + { + $cacheKey = "message_template_{$templateCode}"; + + return Cache::remember($cacheKey, function() use ($templateCode) { + return Db::name('school_message_templates') + ->where('template_code', $templateCode) + ->where('is_active', 1) + ->find(); + }, 600); // 缓存10分钟 + } + + /** + * 渲染模板 + * @param string $template 模板内容 + * @param array $variables 变量 + * @return string + */ + private function renderTemplate($template, $variables) + { + $pattern = '/\{\{(\w+)\}\}/'; + + return preg_replace_callback($pattern, function($matches) use ($variables) { + $key = $matches[1]; + return isset($variables[$key]) ? $variables[$key] : $matches[0]; + }, $template); + } + + /** + * 发送模板消息 + * @param string $templateCode 模板代码 + * @param int $fromId 发送者ID + * @param string $fromType 发送者类型 + * @param int $toId 接收者ID + * @param int $friendId 好友关系ID + * @param array $variables 模板变量 + * @param array $options 额外选项 + * @return int 消息ID + */ + public function sendTemplateMessage($templateCode, $fromId, $fromType, $toId, $friendId, $variables = [], $options = []) + { + $messageData = $this->generateMessage($templateCode, $variables, $options); + + $messageId = Db::name('school_chat_messages')->insertGetId([ + 'from_type' => $fromType, + 'from_id' => $fromId, + 'to_id' => $toId, + 'friend_id' => $friendId, + 'message_type' => $messageData['message_type'], + 'title' => $messageData['title'], + 'content' => $messageData['content'], + 'business_type' => $messageData['business_type'], + 'business_id' => $options['business_id'] ?? null, + 'is_read' => 0, + 'created_at' => date('Y-m-d H:i:s') + ]); + + // 如果开启了微信推送 + if (!empty($options['wechat_push']) && !empty($options['openid'])) { + $this->sendWechatTemplateMessage($messageData, $options['openid'], $options); + } + + return $messageId; + } + + /** + * 发送微信模板消息 + * @param array $messageData 消息数据 + * @param string $openid 微信openid + * @param array $options 选项 + */ + private function sendWechatTemplateMessage($messageData, $openid, $options = []) + { + // 调用微信推送服务 + $wechatService = new WechatPushService(); + $wechatService->sendTemplateMessage($openid, $messageData, $options); + } +} +``` + +#### **3. 微信公众号模板消息推送功能** + +**设计目标**:集成微信公众号模板消息,实现消息的即时推送通知 + +**微信推送服务**: +```php +appId = Config::get('wechat.app_id'); + $this->appSecret = Config::get('wechat.app_secret'); + $this->templateId = Config::get('wechat.template_id.message_notify'); + } + + /** + * 发送模板消息 + * @param string $openid 用户openid + * @param array $messageData 消息数据 + * @param array $options 选项 + * @return bool + */ + public function sendTemplateMessage($openid, $messageData, $options = []) + { + try { + $accessToken = $this->getAccessToken(); + $url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={$accessToken}"; + + $data = [ + 'touser' => $openid, + 'template_id' => $this->templateId, + 'url' => $options['jump_url'] ?? '', + 'miniprogram' => [ + 'appid' => Config::get('wechat.mini_app_id', ''), + 'pagepath' => $this->buildMiniProgramPath($messageData, $options) + ], + 'data' => [ + 'first' => ['value' => $messageData['title']], + 'keyword1' => ['value' => $messageData['message_type_text'] ?? '系统消息'], + 'keyword2' => ['value' => date('Y-m-d H:i:s')], + 'remark' => ['value' => mb_substr($messageData['content'], 0, 100) . '...'] + ] + ]; + + $result = $this->httpPost($url, json_encode($data, JSON_UNESCAPED_UNICODE)); + $response = json_decode($result, true); + + return isset($response['errcode']) && $response['errcode'] === 0; + + } catch (\Exception $e) { + // 记录错误日志 + trace('微信模板消息发送失败: ' . $e->getMessage(), 'error'); + return false; + } + } + + /** + * 构建小程序页面路径 + * @param array $messageData 消息数据 + * @param array $options 选项 + * @return string + */ + private function buildMiniProgramPath($messageData, $options) + { + $basePath = 'pages-student/messages/index'; + + // 根据消息类型构建不同的跳转路径 + if (!empty($options['business_id'])) { + $routeMap = [ + 'order' => 'pages-common/order/detail', + 'student_courses' => 'pages-student/courses/detail', + 'person_course_schedule' => 'pages-student/schedule/detail' + ]; + + if (isset($routeMap[$messageData['message_type']])) { + return $routeMap[$messageData['message_type']] . '?id=' . $options['business_id']; + } + } + + return $basePath . '?student_id=' . $options['student_id']; + } + + /** + * 获取微信访问令牌 + * @return string + */ + private function getAccessToken() + { + $cacheKey = 'wechat_access_token'; + + return Cache::remember($cacheKey, function() { + $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}"; + $result = file_get_contents($url); + $data = json_decode($result, true); + + if (isset($data['access_token'])) { + return $data['access_token']; + } + + throw new \Exception('获取微信访问令牌失败: ' . json_encode($data)); + }, 7000); // 缓存约2小时 + } + + /** + * HTTP POST请求 + * @param string $url + * @param string $data + * @return string + */ + private function httpPost($url, $data) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json; charset=utf-8' + ]); + + $result = curl_exec($ch); + curl_close($ch); + + return $result; + } +} +``` + +#### **4. 搜索功能完整实现(含时间筛选)** + +**后端搜索API**: +```php +=', $start_date . ' 00:00:00']; + } + + if (!empty($end_date)) { + $where[] = ['created_at', '<=', $end_date . ' 23:59:59']; + } + + $list = Db::name('school_chat_messages') + ->alias('cm') + ->leftJoin('school_personnel sp', 'cm.from_id = sp.id AND cm.from_type = "personnel"') + ->where($where) + ->field([ + 'cm.*', + 'CASE + WHEN cm.from_type = "personnel" THEN sp.name + WHEN cm.from_type = "system" THEN "系统" + ELSE "客户" + END as from_name' + ]) + ->order('cm.created_at DESC') + ->paginate([ + 'list_rows' => $limit, + 'page' => $page + ]); + + return [ + 'list' => $list->items(), + 'total' => $list->total(), + 'has_more' => $list->hasMore() + ]; +} +``` + +**前端搜索组件**: ```vue - + - 🔍 + + + + 开始日期: {{ searchForm.start_date || '请选择' }} + + + + + + 结束日期: {{ searchForm.end_date || '请选择' }} + + + + 搜索 + 重置 + ``` -### **2. 添加业务页面跳转** ```javascript -// 修改 viewMessage 方法 -viewMessage(message) { - // 如果有关联业务,直接跳转 - if (message.business_id && message.business_type) { - this.navigateToBusinessPage(message); - } else { - // 否则显示消息详情弹窗 - this.selectedMessage = message; - this.showMessagePopup = true; +// 搜索相关数据和方法 +data() { + return { + searchForm: { + keyword: '', + start_date: '', + end_date: '' + }, + searchTimer: null, + // ... 其他数据 } +}, + +methods: { + performSearch() { + clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(() => { + this.currentPage = 1; + this.searchMessages(); + }, 500); + }, - // 标记为已读 - if (!message.is_read) { - this.markAsRead(message); + async searchMessages() { + this.loading = true; + try { + const response = await apiRoute.searchStudentMessages({ + student_id: this.studentId, + keyword: this.searchForm.keyword, + message_type: this.activeType === 'all' ? '' : this.activeType, + start_date: this.searchForm.start_date, + end_date: this.searchForm.end_date, + page: this.currentPage, + limit: 10 + }); + + if (response && response.code === 1) { + const apiData = response.data; + const newList = this.formatMessageList(apiData.list || []); + + if (this.currentPage === 1) { + this.messagesList = newList; + } else { + this.messagesList = [...this.messagesList, ...newList]; + } + + this.hasMore = apiData.has_more || false; + this.applyTypeFilter(); + } + } catch (error) { + console.error('搜索消息失败:', error); + } finally { + this.loading = false; + } + }, + + onStartDateChange(e) { + this.searchForm.start_date = e.detail.value; + }, + + onEndDateChange(e) { + this.searchForm.end_date = e.detail.value; + }, + + resetSearch() { + this.searchForm = { + keyword: '', + start_date: '', + end_date: '' + }; + this.currentPage = 1; + this.loadMessages(); } } ``` -### **3. 完善消息类型映射** -```javascript -getTypeText(type) { - const typeMap = { - 'text': '文本消息', - 'img': '图片消息', - 'system': '系统消息', - 'notification': '通知公告', - 'homework': '作业任务', - 'feedback': '反馈评价', - 'reminder': '课程提醒', - 'order': '订单消息', - 'student_courses': '课程变动', - 'person_course_schedule': '课程安排' - }; - return typeMap[type] || type; +#### **5. 用户身份统一优化(to_type字段方案)** + +**数据库修改**: +```sql +-- 添加接收者类型字段 +ALTER TABLE `school_chat_messages` +ADD COLUMN `to_type` enum('personnel','customer','student') NOT NULL DEFAULT 'customer' COMMENT '接收者类型' +AFTER `to_id`; + +-- 创建复合索引提升查询性能 +ALTER TABLE `school_chat_messages` +ADD INDEX `idx_to_id_type_time` (`to_id`, `to_type`, `created_at`), +ADD INDEX `idx_to_type_read` (`to_type`, `is_read`); + +-- 数据迁移:根据现有数据推断to_type值 +-- 这里需要根据实际的业务逻辑来更新数据 +UPDATE school_chat_messages +SET to_type = 'customer' +WHERE to_type = 'customer'; -- 默认设置,需要根据实际情况调整 +``` + +**查询优化**: +```php +where($where) + ->order('created_at DESC') + ->paginate([ + 'list_rows' => $limit, + 'page' => $page + ]); } ``` -## 📊 **总结** +### **第二阶段:性能优化和扩展功能(中优先级)** + +1. **消息队列机制**:使用Redis队列处理大量消息发送 +2. **数据分片策略**:按月份分表存储历史消息 +3. **缓存策略优化**:热点消息和统计数据缓存 +4. **API接口优化**:批量查询、分页优化 -### **主要问题** -1. ❌ **消息类型枚举不匹配**:数据库枚举与前端期望完全不同 -2. ❌ **缺少已读状态字段**:无法实现已读/未读功能 -3. ❌ **缺少搜索功能**:前端没有搜索框,后端没有搜索接口 -4. ❌ **缺少业务跳转逻辑**:无法根据消息类型跳转到业务页面 -5. ❌ **缺少消息统计字段**:chat_friends 表无法统计消息数量 +### **实施时间线** -### **修复优先级** -1. **高优先级**:修改消息类型枚举,添加已读状态字段 -2. **中优先级**:添加搜索功能,完善业务跳转逻辑 -3. **低优先级**:添加消息统计字段和触发器 +- **第1周**:消息路由系统 + to_type字段优化 +- **第2周**:消息模板系统 + 搜索功能 +- **第3周**:微信推送功能集成 +- **第4周**:测试、优化和部署 -### **建议** -建议先修改数据库结构,然后完善前端功能,最后实现后端接口,确保学员端消息管理功能完整可用。 +这个架构优化方案完全基于你的需求和回复制定,可以立即开始实施。每个功能都有完整的代码实现,你觉得哪个部分需要进一步细化?