From 474d8a28e4d9694aa7bc2fb775f0c1240854a512 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Tue, 12 Aug 2025 16:46:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- niucloud/app/api/controller/Dashboard.php | 423 +--- .../controller/apiController/Personnel.php | 54 + niucloud/app/api/route/route.php | 3 + .../api/apiService/PersonnelService.php | 94 +- .../SchoolApprovalProcessService.php | 66 + uniapp/api/apiRoute.js | 7 + uniapp/pages-market/clue/add_clues.vue | 88 +- uniapp/pages-market/clue/edit_clues.vue | 95 +- uniapp/pages-market/reimbursement/add.vue | 197 +- 学员端消息管理数据库分析报告.md | 1818 ----------------- 学员端知识库模块详细开发任务.md | 574 ------ 11 files changed, 597 insertions(+), 2822 deletions(-) delete mode 100644 学员端消息管理数据库分析报告.md delete mode 100644 学员端知识库模块详细开发任务.md diff --git a/niucloud/app/api/controller/Dashboard.php b/niucloud/app/api/controller/Dashboard.php index 2b992317..e34a3c12 100644 --- a/niucloud/app/api/controller/Dashboard.php +++ b/niucloud/app/api/controller/Dashboard.php @@ -4,6 +4,7 @@ namespace app\api\controller; use app\service\api\member\MemberService; use core\base\BaseApiController; +use think\facade\View; /** * Dashboard WebView 控制器 @@ -61,8 +62,13 @@ class Dashboard extends BaseApiController $pageTitle = $titleMap[$type] ?? '数据统计'; - // 生成HTML内容 - return $this->generateHTML($pageTitle, $pageData, $platform); + // 使用视图模板渲染页面 + return View::fetch('dashboard/main', [ + 'pageTitle' => $pageTitle, + 'pageData' => $pageData, + 'platform' => $platform, + 'userInfo' => $userInfo + ]); } /** @@ -179,420 +185,17 @@ class Dashboard extends BaseApiController ]; } - /** - * 生成HTML内容 - */ - private function generateHTML($title, $data, $platform) - { - $statsHtml = ''; - if (!empty($data['stats'])) { - foreach ($data['stats'] as $stat) { - $trendColor = strpos($stat['trend'], '+') === 0 ? '#4CAF50' : '#FF5722'; - $statsHtml .= " -
-
{$stat['label']}
-
{$stat['value']}{$stat['unit']}
-
{$stat['trend']}
-
- "; - } - } - - // 生成图表数据的JavaScript - $chartsScript = ''; - if (!empty($data['charts'])) { - foreach ($data['charts'] as $chartId => $chart) { - $chartData = json_encode($chart['data']); - $chartsScript .= " - renderChart('{$chartId}', '{$chart['title']}', {$chartData}); - "; - } - } - - return " - - - - - - {$title} - - - - -
-

{$title}

- -
- {$statsHtml} -
- -
-
- - - - - -"; - } /** * 渲染错误页面 */ private function renderErrorPage($message) { - return response(" - - - - - - 页面错误 - - - -
-
⚠️
-
页面加载失败
-
{$message}
-
- -")->header([ + $errorHtml = View::fetch('dashboard/error', [ + 'message' => $message + ]); + + return response($errorHtml)->header([ 'Content-Type' => 'text/html; charset=utf-8' ]); } diff --git a/niucloud/app/api/controller/apiController/Personnel.php b/niucloud/app/api/controller/apiController/Personnel.php index ea0a4e98..00837766 100644 --- a/niucloud/app/api/controller/apiController/Personnel.php +++ b/niucloud/app/api/controller/apiController/Personnel.php @@ -159,7 +159,61 @@ class Personnel extends BaseApiService 'receipt_url' => $request->param('receipt_url', ''), ]; return success((new PersonnelService())->reimbursement_add($data)); + } + /** + * 添加报销申请(带审批流程) + * @param Request $request + * @return mixed + */ + public function reimbursementAddWithApproval(Request $request) + { + $params = $request->all(); + + // 验证必填字段 + if (empty($params['amount'])) { + return fail('报销金额不能为空'); + } + + if (empty($params['description'])) { + return fail('报销描述不能为空'); + } + + // 验证金额格式 + if (!is_numeric($params['amount']) || floatval($params['amount']) <= 0) { + return fail('请输入正确的报销金额'); + } + + try { + $res = (new PersonnelService())->reimbursementAddWithApproval($params); + if (!$res['code']) { + return fail($res['msg']); + } + + return success($res['data'], $res['msg']); + } catch (\Exception $e) { + return fail('添加报销申请失败:' . $e->getMessage()); + } + } + + /** + * 获取报销审批配置列表 + * @param Request $request + * @return mixed + */ + public function getReimbursementApprovalConfigs(Request $request) + { + try { + $params = $request->all(); + $businessType = $params['business_type'] ?? 'expense_approval'; + + $approvalService = new \app\service\school_approval\SchoolApprovalConfigService(); + $configs = $approvalService->getActiveConfigs($businessType); + + return success($configs); + } catch (\Exception $e) { + return fail('获取审批配置失败:' . $e->getMessage()); + } } /** diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 790e3a1a..bb18b576 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -434,6 +434,9 @@ Route::group(function () { Route::get('personnel/reimbursement_list', 'apiController.Personnel/reimbursement_list'); Route::post('personnel/reimbursement_add', 'apiController.Personnel/reimbursement_add'); Route::get('personnel/reimbursement_info', 'apiController.Personnel/reimbursement_info'); + //报销审批流程 + Route::post('personnel/reimbursement_add_with_approval', 'apiController.Personnel/reimbursementAddWithApproval'); + Route::get('personnel/reimbursement/approval-configs', 'apiController.Personnel/getReimbursementApprovalConfigs'); //合同管理 Route::get('contract/myContracts', 'apiController.Contract/myContracts'); diff --git a/niucloud/app/service/api/apiService/PersonnelService.php b/niucloud/app/service/api/apiService/PersonnelService.php index 9437221f..85e66246 100644 --- a/niucloud/app/service/api/apiService/PersonnelService.php +++ b/niucloud/app/service/api/apiService/PersonnelService.php @@ -399,14 +399,104 @@ class PersonnelService extends BaseApiService 'applicant_id' => $this->member_id, 'amount' => $data['amount'], 'description' => $data['description'], - 'receipt_url' => $data['receipt_url'] + 'receipt_url' => $data['receipt_url'], + 'status' => 'pending' ]); } + return true; + } + /** + * 添加报销申请(带审批流程) + * @param array $data + * @return array + */ + public function reimbursementAddWithApproval(array $data) + { + $res = [ + 'code' => 0, + 'msg' => '添加失败', + 'data' => [] + ]; - return true; + try { + // 开启事务 + \think\facade\Db::startTrans(); + + // 创建报销记录 + $reimbursementData = [ + 'applicant_id' => $this->member_id, + 'amount' => $data['amount'], + 'description' => $data['description'], + 'receipt_url' => $data['receipt_url'] ?? '', + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s') + ]; + + $reimbursement = new Reimbursement(); + $reimbursementId = $reimbursement->insertGetId($reimbursementData); + + if (!$reimbursementId) { + \think\facade\Db::rollback(); + $res['msg'] = '创建报销记录失败'; + return $res; + } + // 检查是否使用审批流程 + if (isset($data['use_approval']) && $data['use_approval'] && + isset($data['approval_config_id']) && $data['approval_config_id'] > 0) { + + // 启动审批流程 + $approvalService = new \app\service\school_approval\SchoolApprovalProcessService(); + $processData = [ + 'process_name' => '报销申请 - ' . $data['description'], + 'applicant_id' => $this->member_id, + 'remarks' => $data['remarks'] ?? '报销申请', + 'business_type' => 'expense_approval', + 'business_id' => $reimbursementId, + 'business_data' => $data + ]; + + $processId = $approvalService->create($processData, $data['approval_config_id']); + + // 更新报销记录关联审批流程 + $reimbursement->where(['id' => $reimbursementId])->update([ + 'process_id' => $processId + ]); + + \think\facade\Db::commit(); + + $res = [ + 'code' => 1, + 'msg' => '报销申请已提交,等待审批', + 'data' => [ + 'type' => 'approval', + 'reimbursement_id' => $reimbursementId, + 'process_id' => $processId + ] + ]; + } else { + // 直接创建(无审批) + \think\facade\Db::commit(); + + $res = [ + 'code' => 1, + 'msg' => '报销记录创建成功', + 'data' => [ + 'type' => 'direct', + 'reimbursement_id' => $reimbursementId + ] + ]; + } + + } catch (\Exception $e) { + \think\facade\Db::rollback(); + $res['msg'] = '创建报销申请失败:' . $e->getMessage(); + } + + return $res; } /** diff --git a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php index 3300cb1b..8493589f 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php +++ b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php @@ -367,6 +367,9 @@ class SchoolApprovalProcessService case 'personnel_add': $this->handlePersonnelAddApproval($process); break; + case 'expense_approval': + $this->handleExpenseApproval($process); + break; default: // 其他业务类型的处理逻辑 break; @@ -455,6 +458,9 @@ class SchoolApprovalProcessService case 'personnel_add': // 人员添加被拒绝,不需要特殊处理 break; + case 'expense_approval': + $this->handleExpenseRejected($process); + break; default: // 其他业务类型的处理逻辑 break; @@ -726,4 +732,64 @@ class SchoolApprovalProcessService throw new \Exception('创建人员角色绑定关系失败:' . $e->getMessage()); } } + + /** + * 处理报销审批完成 + * @param $process + * @throws \Exception + */ + private function handleExpenseApproval($process): void + { + if (empty($process['business_id'])) { + throw new Exception('报销ID不存在'); + } + + try { + // 更新报销记录状态为已审核 + $reimbursementModel = new \app\model\reimbursement\Reimbursement(); + $updateResult = $reimbursementModel->where(['id' => $process['business_id']]) + ->update([ + 'status' => 'approved', + 'updated_at' => date('Y-m-d H:i:s') + ]); + + if (!$updateResult) { + throw new Exception('更新报销记录状态失败'); + } + + \think\facade\Log::info("报销审批完成,报销ID: {$process['business_id']},流程ID: {$process['id']}"); + + } catch (\Exception $e) { + throw new Exception('处理报销审批完成失败:' . $e->getMessage()); + } + } + + /** + * 处理报销审批拒绝 + * @param $process + * @throws \Exception + */ + private function handleExpenseRejected($process): void + { + if (empty($process['business_id'])) { + return; + } + + try { + // 更新报销记录状态为已拒绝 + $reimbursementModel = new \app\model\reimbursement\Reimbursement(); + $updateResult = $reimbursementModel->where(['id' => $process['business_id']]) + ->update([ + 'status' => 'rejected', + 'updated_at' => date('Y-m-d H:i:s') + ]); + + if ($updateResult) { + \think\facade\Log::info("报销审批拒绝,报销ID: {$process['business_id']},流程ID: {$process['id']}"); + } + + } catch (\Exception $e) { + \think\facade\Log::error('处理报销审批拒绝失败:' . $e->getMessage()); + } + } } diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index 31c3852b..ab77fae2 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -664,6 +664,13 @@ export default { async reimbursement_info(data = {}) { return await http.get('/personnel/reimbursement_info', data) }, + // 报销审批相关接口 + async reimbursement_add_with_approval(data = {}) { + return await http.post('/personnel/reimbursement_add_with_approval', data) + }, + async reimbursementApprovalConfigs(data = {}) { + return await http.get('/personnel/reimbursement/approval-configs', data) + }, async schedule_del(data = {}) { return await http.post('/course/schedule_del', data) }, diff --git a/uniapp/pages-market/clue/add_clues.vue b/uniapp/pages-market/clue/add_clues.vue index 086d82b0..7f99c75b 100644 --- a/uniapp/pages-market/clue/add_clues.vue +++ b/uniapp/pages-market/clue/add_clues.vue @@ -393,7 +393,7 @@ + @click="openDateTime(`promised_visit_time`)"> {{ (formData.promised_visit_time) ? formData.promised_visit_time : '点击选择' }} @@ -478,6 +478,17 @@ @cancel="cancel_date"> + + + + { + this.datetime_picker_show = true + }) + }, //选择跟进时间 change_date(e) { console.log('日期选择器返回数据:', e) @@ -1677,6 +1721,48 @@ export default { this.date_picker_show = false }, + //选择日期时间 + change_datetime(e) { + console.log('日期时间选择器返回数据:', e) + + // 获取选择的日期时间值,兼容不同的返回格式 + let val = '' + if (e.result) { + val = e.result + } else if (e.value) { + val = e.value + } else if (e.detail && e.detail.result) { + val = e.detail.result + } else if (e.detail && e.detail.value) { + val = e.detail.value + } + + // 确保日期时间格式为 YYYY-MM-DD HH:mm + if (val && typeof val === 'string') { + // 如果是时间戳,转换为日期时间字符串 + if (/^\d+$/.test(val)) { + const date = new Date(parseInt(val)) + val = this.formatDateTime(date) + } + // 统一格式为 YYYY-MM-DD HH:mm + if (val.includes('/')) { + // 处理 YYYY/MM/DD HH:mm 格式 + val = val.replace(/\//g, '-') + } + } + + let input_name = this.datetime_picker_input_name + this.formData[input_name] = val + + console.log(`设置${input_name}为:`, val) + this.cancel_datetime() + }, + + //关闭日期时间选择器 + cancel_datetime() { + this.datetime_picker_show = false + }, + //下一步 index|0=添加客户,1六要素 async nextStep(index) { diff --git a/uniapp/pages-market/clue/edit_clues.vue b/uniapp/pages-market/clue/edit_clues.vue index f4f08f31..94a581d8 100644 --- a/uniapp/pages-market/clue/edit_clues.vue +++ b/uniapp/pages-market/clue/edit_clues.vue @@ -95,7 +95,7 @@ - + @@ -167,7 +167,7 @@ - + {{ formData.promised_visit_time ? formData.promised_visit_time : '点击选择' }} @@ -219,9 +219,9 @@ - + - + @@ -307,6 +307,10 @@ + + + + @@ -389,7 +393,8 @@ first_visit_status: '', //一访情况 second_visit_status: '', //二访情况 efficacious:'1', - call_intent:'2' + call_intent: '', // 不设置默认值,让后端数据正常回显 + emotional_stickiness_score: '' // 情感粘度 }, //下拉选择器相关 @@ -472,12 +477,22 @@ text: '', options: [], }, + //情感粘度 + emotional_stickiness_score: { + text: '', + options: [], + }, }, //选择器选项配置 // 年月日选择组件 data_picker_input_name: '', //时间组件的input_name date_picker_show: false, //时间选择器是否展示 default_date_value: '', // 添加默认日期值 + + // 日期时间选择组件 + datetime_picker_input_name: '', //日期时间组件的input_name + datetime_picker_show: false, //日期时间选择器是否展示 + default_datetime_value: '', // 默认日期时间值 @@ -736,17 +751,24 @@ is_bm: sixSpeed.is_bm || 2, // 是否报名,默认未报名 efficacious: sixSpeed.efficacious || '', call_intent: sixSpeed.call_intent || '', // 是否加微信 + emotional_stickiness_score: sixSpeed.emotional_stickiness_score || '', // 情感粘度 }; console.log('getInfo - 表单数据设置完成:', this.formData); // 格式化日期时间 if (sixSpeed.promised_visit_time) { - this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d'); + // 如果包含时间部分,保持原格式;否则只格式化日期部分 + if (sixSpeed.promised_visit_time.includes(' ')) { + this.formData.promised_visit_time = sixSpeed.promised_visit_time; + } else { + this.formData.promised_visit_time = this.$util.formatToDateTime(sixSpeed.promised_visit_time, 'Y-m-d'); + } } + // 可选上课时间保持原始文本格式,不做日期格式化 if (sixSpeed.preferred_class_time) { - this.formData.optional_class_time = this.$util.formatToDateTime(sixSpeed.preferred_class_time, 'Y-m-d'); + this.formData.optional_class_time = sixSpeed.preferred_class_time; } if (sixSpeed.first_visit_time) { @@ -792,6 +814,7 @@ this.setPickerTextByValue('purchasing_power', this.formData.purchasing_power, sixSpeed.purchase_power_name); this.setPickerTextByValue('cognitive_idea', this.formData.cognitive_idea, sixSpeed.concept_awareness_name); this.setPickerTextByValue('distance', this.formData.distance, sixSpeed.distance_name); + this.setPickerTextByValue('emotional_stickiness_score', this.formData.emotional_stickiness_score, sixSpeed.emotional_stickiness_score_name); // 不再需要设置call_intent的选择器文本,因为已改为单选组件 console.log('选择器文本回显完成'); @@ -1156,6 +1179,13 @@ this.date_picker_show = true; }, + // 打开日期时间选择器 + openDateTime(inputName) { + this.datetime_picker_input_name = inputName; + this.setDefaultDateTimeValue(inputName); + this.datetime_picker_show = true; + }, + // 设置默认日期值 setDefaultDateValue(inputName) { if (this.formData[inputName]) { @@ -1166,6 +1196,16 @@ } }, + // 设置默认日期时间值 + setDefaultDateTimeValue(inputName) { + if (this.formData[inputName]) { + this.default_datetime_value = this.formData[inputName]; + } else { + const now = new Date(); + this.default_datetime_value = this.formatDateTimeToString(now); + } + }, + // 格式化日期为字符串 formatDateToString(date) { const year = date.getFullYear(); @@ -1174,6 +1214,16 @@ return `${year}-${month}-${day}`; }, + // 格式化日期时间为字符串 + formatDateTimeToString(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; + }, + // 处理日期选择 change_date(e) { const val = e.result || ''; @@ -1188,6 +1238,37 @@ this.date_picker_show = false; }, + // 处理日期时间选择 + change_datetime(e) { + console.log('日期时间选择器返回数据:', e); + + let val = e.result || e.value || ''; + + // 确保日期时间格式正确 + if (val && typeof val === 'string') { + // 如果包含时间部分但格式不对,进行格式化 + if (val.includes(' ') || val.includes('T')) { + try { + const date = new Date(val); + if (!isNaN(date.getTime())) { + val = this.formatDateTimeToString(date); + } + } catch (error) { + console.warn('日期时间格式化失败:', error); + } + } + } + + const inputName = this.datetime_picker_input_name; + this.formData[inputName] = val; + this.cancel_datetime(); + }, + + // 关闭日期时间选择器 + cancel_datetime() { + this.datetime_picker_show = false; + }, + //下一步 index|0=添加客户,1六要素 async nextStep(index) { this.optionTableId = String(index) diff --git a/uniapp/pages-market/reimbursement/add.vue b/uniapp/pages-market/reimbursement/add.vue index 6b330df9..7d1a737f 100644 --- a/uniapp/pages-market/reimbursement/add.vue +++ b/uniapp/pages-market/reimbursement/add.vue @@ -30,8 +30,30 @@ + + + + 审批流程 + + + 当前配置: + {{ approvalData.selectedConfig.config_name || '报销审批' }} + + + 流程说明: + {{ approvalData.selectedConfig.description || '报销审批流程' }} + + + 提交后将自动进入审批流程,请等待审批完成 + + + + - 提交 + + {{ submitting ? '提交中...' : '提交' }} + @@ -47,18 +69,30 @@ uploadUrl: `${Api_url}/uploadImage`, pageTitle: '新增报销', disabled: false, + submitting: false, + loadingApprovalConfigs: false, form: { id: null, amount: '', description: '', receipt_url: '', - } + }, + // 审批流程相关数据 + approvalData: { + useApproval: true, // 固定启用审批流程 + selectedConfig: null, + selectedIndex: 0 + }, + approvalConfigs: [] } }, onLoad(options) { if (options.id) { this.form.id = options.id; this.fetchDetail(options.id); + } else { + // 新增报销时加载审批配置 + this.loadApprovalConfigs(); } }, methods: { @@ -131,7 +165,48 @@ }); } }, - submit() { + // 加载审批配置 + async loadApprovalConfigs() { + this.loadingApprovalConfigs = true; + try { + const response = await apiRoute.reimbursementApprovalConfigs({ + business_type: 'expense_approval' + }) + if (response.code === 1) { + this.approvalConfigs = response.data || [] + // 自动选择第一个可用的审批配置 + if (this.approvalConfigs.length > 0) { + this.approvalData.selectedConfig = this.approvalConfigs[0] + this.approvalData.selectedIndex = 0 + } + console.log('审批配置加载成功:', this.approvalConfigs) + } else { + console.error('审批配置加载失败:', response.msg) + // 设置默认配置避免阻塞提交 + this.approvalData.selectedConfig = { + id: 0, + config_name: '报销审批', + description: '报销审批流程', + business_type: 'expense_approval' + } + } + } catch (error) { + console.error('加载审批配置失败:', error) + // 网络错误时也设置默认配置 + this.approvalData.selectedConfig = { + id: 0, + config_name: '报销审批', + description: '报销审批流程', + business_type: 'expense_approval' + } + } finally { + this.loadingApprovalConfigs = false; + } + }, + + + async submit() { + // 表单验证 if (!this.form.amount || !this.form.description) { uni.showToast({ title: '请填写完整', @@ -139,18 +214,71 @@ }); return; } - let res = apiRoute.reimbursement_add(this.form) - if (res['code'] == 1) { + // 验证金额格式 + if (parseFloat(this.form.amount) <= 0) { uni.showToast({ - title: '提交成功', - icon: 'success' + title: '请输入正确的报销金额', + icon: 'none' }); + return; } - setTimeout(() => { - uni.navigateBack(); - }, 1000); + // 确保有可用的审批配置 + if (!this.approvalData.selectedConfig) { + uni.showToast({ + title: '审批配置加载失败,请稍后重试', + icon: 'none' + }); + return; + } + + this.submitting = true; + + try { + // 准备提交数据 + const submitData = { + ...this.form, + use_approval: true, // 固定为true + approval_config_id: this.approvalData.selectedConfig.id + }; + + console.log('提交报销数据:', submitData); + + let response; + if (this.form.id) { + // 编辑模式,使用普通接口 + response = await apiRoute.reimbursement_add(submitData); + } else { + // 新增模式,固定使用审批接口 + response = await apiRoute.reimbursement_add_with_approval(submitData); + } + + if (response.code === 1) { + uni.showToast({ + title: '报销申请已提交,等待审批', + icon: 'success', + duration: 2000 + }); + + setTimeout(() => { + uni.navigateBack(); + }, 2000); + } else { + uni.showToast({ + title: response.msg || '提交失败', + icon: 'none' + }); + } + } catch (error) { + console.error('提交报销失败:', error); + uni.showToast({ + title: '网络错误,请稍后重试', + icon: 'none' + }); + } finally { + this.submitting = false; + } } } } @@ -269,4 +397,53 @@ padding: 20rpx 40rpx 40rpx 40rpx; box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.12); } + + /* 审批流程样式 */ + .approval-info-section { + margin-top: 20rpx; + } + + .section-title { + font-size: 32rpx; + color: #fff; + font-weight: bold; + margin-bottom: 32rpx; + padding-left: 16rpx; + border-left: 6rpx solid #24BA9F; + } + + .approval-info { + margin-top: 24rpx; + padding: 24rpx; + background: #3a3a3a; + border-radius: 12rpx; + } + + .approval-info .info-item { + margin-bottom: 16rpx; + } + + .approval-info .info-label { + color: #aaa; + font-size: 26rpx; + } + + .approval-info .info-value { + color: #24BA9F; + font-size: 26rpx; + margin-left: 8rpx; + } + + .approval-info .info-note { + padding: 16rpx; + background: #2a2a2a; + border-radius: 8rpx; + border-left: 4rpx solid #24BA9F; + } + + .approval-info .info-note text { + color: #999; + font-size: 24rpx; + line-height: 1.5; + } \ No newline at end of file diff --git a/学员端消息管理数据库分析报告.md b/学员端消息管理数据库分析报告.md deleted file mode 100644 index d51fd551..00000000 --- a/学员端消息管理数据库分析报告.md +++ /dev/null @@ -1,1818 +0,0 @@ -# 消息管理数据库分析报告(移动端+管理端) - -## 📋 **需求分析** - -### **第一部分:UniApp移动端消息管理** - -基于 `pages-student/messages/index.vue` 页面分析,移动端消息管理需要实现: - -1. **消息列表展示**:显示员工/学员接收的所有消息 ✅ -2. **消息类型筛选**:按消息类型进行分类筛选 ✅ -3. **消息已读状态**:标记消息已读/未读 ✅ -4. **消息数量统计**:显示未读消息数量和总消息数 ✅ -5. **业务页面跳转**:根据消息类型跳转到对应业务页面 ❌(需要完善) -6. **消息搜索功能**:关键词搜索消息内容 ❌(需要添加) - -### **第二部分:Admin管理端客户详情消息展示** - -基于 `admin/src/app/views/customer_resources/components/UserProfile.vue` 分析,管理端需要在客户详情中展示: - -1. **消息历史记录**:显示与该客户的所有消息记录 ❌(需要添加) -2. **消息统计信息**:已读/未读消息数量统计 ❌(需要添加) -3. **消息时间轴**:按时间顺序展示消息流 ❌(需要添加) -4. **消息类型展示**:不同类型消息的分类展示 ❌(需要添加) - -## 🗄️ **数据库表结构分析** - -### **1. school_chat_messages 表** - -#### **现有字段分析** -基于数据库实际结构分析,该表包含以下字段: -- `id` - 主键ID ✅ -- `from_type` - 发送者类型(enum: personnel, customer, system) ✅ -- `from_id` - 发送者ID ✅ -- `to_id` - 接收者ID ✅ -- `friend_id` - 关联 chat_friends 表ID ✅ -- `message_type` - 消息类型 ✅**(已包含完整枚举值)** -- `content` - 消息内容 ✅ -- `is_read` - 是否已读 ✅ -- `read_time` - 已读时间 ✅ -- `title` - 消息标题 ✅ -- `business_id` - 关联业务ID ✅ -- `business_type` - 业务类型 ✅ -- `created_at`/`updated_at` - 时间戳 ✅ -- `delete_time` - 删除时间(软删除) ✅ - -#### **✅ 字段状态:完善** -经过数据库检查,发现所有必要字段都已存在,包括: -- 已读状态跟踪:`is_read`, `read_time` -- 业务关联:`business_id`, `business_type`, `title` -- 消息类型枚举已包含所有需要的类型: - ```sql - enum('text','img','system','notification','homework','feedback','reminder','order','student_courses','person_course_schedule') - ``` - -### **2. school_chat_friends 表** - -#### **现有字段分析** -基于数据库实际结构分析: -- `id` - 主键ID ✅ -- `personnel_id` - 员工ID ✅ -- `customer_resources_id` - 客户资源ID ✅ -- `unread_count_personnel` - 员工端未读消息数 ✅ -- `unread_count_customer_resources` - 客户端未读消息数 ✅ -- `created_at`/`updated_at` - 时间戳 ✅ -- `delete_time` - 删除时间(软删除) ✅ - -#### **⚠️ 需要优化的字段** -虽然基本字段完整,但建议添加以下字段以增强管理端展示效果: -```sql --- 增强统计字段(可选) -ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间'; -ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容'; -ALTER TABLE `school_chat_friends` ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数'; -``` - -## ✅ **数据库设计验证结果** - -### **1. 消息类型枚举完全匹配** - -#### **数据库实际枚举值** -```sql -message_type enum('text','img','system','notification','homework','feedback','reminder','order','student_courses','person_course_schedule') -- text:文本消息 -- img:图片消息 -- system:系统消息 -- notification:通知公告 -- homework:作业任务 -- feedback:反馈评价 -- reminder:课程提醒 -- order:订单消息 -- student_courses:学员课程变动消息 -- person_course_schedule:课程安排消息 -``` - -#### **前端使用的类型** -```javascript -typeTabs: [ - { value: 'all', text: '全部', count: 0 }, - { value: 'system', text: '系统消息', count: 0 }, - { value: 'notification', text: '通知公告', count: 0 }, - { value: 'homework', text: '作业任务', count: 0 }, - { value: 'feedback', text: '反馈评价', count: 0 }, - { value: 'reminder', text: '课程提醒', count: 0 } -] -``` - -**✅ 验证结果**:数据库枚举值与前端期望完全匹配!数据库设计正确。 - -## 🚀 **功能完善建议** - -### **第一部分:UniApp移动端功能增强** - -#### **1. 业务页面跳转逻辑** - -**当前状态**:`viewMessage()` 方法只显示消息详情弹窗 -**需要增强**:根据消息类型和业务关联跳转到对应页面 - -**建议实现方案**: -```javascript -// 在 pages-student/messages/index.vue 中添加 -navigateToBusinessPage(message) { - // 消息类型与业务页面映射 - const routeMap = { - 'order': '/pages-common/order/detail', // 订单详情 - 'student_courses': '/pages-student/courses/detail', // 课程详情 - 'person_course_schedule': '/pages-student/schedule/detail', // 课程安排 - 'homework': '/pages-student/homework/detail', // 作业详情 - 'notification': '/pages-student/announcement/detail', // 通知详情 - 'reminder': '/pages-student/schedule/detail' // 课程提醒 - }; - - const route = routeMap[message.message_type]; - if (route && message.business_id) { - uni.navigateTo({ - url: `${route}?id=${message.business_id}` - }); - } else { - // 没有业务关联时显示详情弹窗 - this.selectedMessage = message; - this.showMessagePopup = true; - } -}, - -// 修改现有的 viewMessage 方法 -viewMessage(message) { - // 标记为已读 - if (!message.is_read) { - this.markAsRead(message); - } - - // 如果有业务关联,直接跳转 - if (message.business_id && message.business_type) { - this.navigateToBusinessPage(message); - } else { - // 否则显示消息详情弹窗 - this.selectedMessage = message; - this.showMessagePopup = true; - } -} -``` - -#### **2. 消息搜索功能** - -**需要添加**:消息内容和标题的关键词搜索 - -**建议实现方案**: -```vue - - - - - 🔍 - - -``` - -```javascript -// 添加搜索相关数据和方法 -data() { - return { - searchKeyword: '', - searchTimer: null, - // ... 其他数据 - } -}, - -methods: { - performSearch() { - // 防抖搜索 - clearTimeout(this.searchTimer); - this.searchTimer = setTimeout(() => { - this.currentPage = 1; - this.loadMessages(); - }, 500); - }, - - // 修改 loadMessages 方法,增加搜索参数 - async loadMessages() { - // ...现有代码 - const response = await apiRoute.getStudentMessageList({ - student_id: this.studentId, - message_type: this.activeType === 'all' ? '' : this.activeType, - search_keyword: this.searchKeyword, // 新增搜索参数 - page: this.currentPage, - limit: 10 - }); - // ...其余代码 - } -} -``` - -### **第二部分:Admin管理端客户详情消息展示** - -#### **1. 在客户详情中增加消息标签页** - -**文件位置**:`admin/src/app/views/customer_resources/components/UserProfile.vue` - -**需要修改的地方**: -```vue - - - - - - - - - - - - - - - - -``` - -#### **2. 创建消息记录组件** - -**新建文件**:`admin/src/app/views/customer_resources/components/Messages.vue` - -```vue - - - - - -``` - -## 🔧 **数据库优化方案** - -### **✅ 核心表结构已完善** - -经过验证,`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大概是这样设计的如果你有更好的方案可以在下一个版本的 -文档中给我说明 -- 是否需要支持批量消息发送?不需要 - -### **5. 数据库字段补充** -**问题**:是否需要立即添加以下字段 -```sql --- 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` -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. 业务页面跳转详细实现** - -#### **消息类型处理策略** -基于你的回复,实现以下跳转逻辑: - -```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 `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 '总消息数'; - --- 创建索引优化查询性能 -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 -); -``` - -#### **自动维护触发器(完善版)** -```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 = 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 - -- 根据消息发送方向更新对应的未读数 - 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. 消息路由系统** - -**设计目标**:建立统一的消息分发和路由机制,支持多种消息类型的自动化处理 - -**数据库设计**: -```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 || '请选择' }} - - - - 搜索 - 重置 - - - -``` - -```javascript -// 搜索相关数据和方法 -data() { - return { - searchForm: { - keyword: '', - start_date: '', - end_date: '' - }, - searchTimer: null, - // ... 其他数据 - } -}, - -methods: { - performSearch() { - clearTimeout(this.searchTimer); - this.searchTimer = setTimeout(() => { - this.currentPage = 1; - this.searchMessages(); - }, 500); - }, - - 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(); - } -} -``` - -#### **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周**:消息路由系统 + to_type字段优化 -- **第2周**:消息模板系统 + 搜索功能 -- **第3周**:微信推送功能集成 -- **第4周**:测试、优化和部署 - -这个架构优化方案完全基于你的需求和回复制定,可以立即开始实施。每个功能都有完整的代码实现,你觉得哪个部分需要进一步细化? diff --git a/学员端知识库模块详细开发任务.md b/学员端知识库模块详细开发任务.md deleted file mode 100644 index f452cebc..00000000 --- a/学员端知识库模块详细开发任务.md +++ /dev/null @@ -1,574 +0,0 @@ -# 学员端知识库模块详细开发任务 - -## 📋 **功能需求分析** - -基于 `pages-student/knowledge/index.vue` 页面分析,知识库模块需要实现以下功能: - -### **前端页面功能** -1. **知识文章列表展示**:支持分页加载 -2. **分类筛选**:按知识库类型筛选内容 -3. **推荐文章**:首页推荐优质内容 -4. **搜索功能**:关键词搜索文章 -5. **收藏功能**:收藏/取消收藏文章 -6. **阅读状态**:标记文章已读 -7. **统计信息**:总文章数、收藏数统计 -8. **文章详情**:跳转到详情页面查看 - -### **数据表结构** -- **主表**:`school_lesson_course_teaching` - 知识库内容表 -- **权限控制**:通过 `student_ids` 字段(逗号分割)控制访问权限 -- **分类字段**:`table_type` 字段区分不同类型的知识内容 - -### **实际数据库字段** -基于 `school_lesson_course_teaching` 表的实际结构: -- `id` - 主键ID -- `title` - 标题 -- `image` - 图片/封面 -- `type` - 类型 -- `url` - 链接地址 -- `content` - 内容(富文本) -- `status` - 状态 -- `create_time` - 创建时间 -- `update_time` - 更新时间 -- `delete_time` - 删除时间(软删除) -- `table_type` - 分类类型(1-29枚举值) -- `user_permission` - 用户权限 -- `exam_papers_id` - 试卷ID -- `student_ids` - 学员权限控制(逗号分割) - -### **table_type 枚举值详细说明** -根据数据库字段注释,`table_type` 包含以下29种类型: - -**教案库类型(1-8)**: -- `1`:课程教学大纲 -- `2`:跳绳教案库 -- `3`:增高教案库 -- `4`:篮球教案库 -- `5`:强化教案库 -- `6`:空中忍者教案库 -- `7`:少儿安防教案库 -- `8`:体能教案库 - -**动作库类型(9-12, 17-22)**: -- `9`:热身动作库 -- `10`:体能动作库 -- `11`:趣味游戏库 -- `12`:放松动作库 -- `17`:空中忍者动作 -- `18`:篮球动作 -- `19`:跳绳动作 -- `20`:跑酷动作 -- `21`:安防动作 -- `22`:标准化动作 - -**训练内容类型(13-16)**: -- `13`:训练内容 -- `14`:训练视频 -- `15`:课后作业 -- `16`:优秀一堂课 - -**体测相关类型(23-26)**: -- `23`:3-6岁体测 -- `24`:7+体测 -- `25`:3-6岁体测讲解—解读 -- `26`:7+岁体测讲解—解读 - -**游戏互动类型(27-29)**: -- `27`:互动游戏 -- `28`:套圈游戏 -- `29`:鼓励方式 - -## 🔧 **后端开发任务细化** - -### **任务1:知识库基础接口开发** -**负责人**:后端开发者 -**工期**:1天 -**优先级**:高 - -#### **1.1 获取知识文章列表接口** -```php -GET /api/student/knowledge/list/{student_id} -``` - -**请求参数**: -- `student_id` (必填): 学员ID -- `category` (可选): 分类筛选,对应 table_type 字段 -- `page` (可选): 页码,默认1 -- `limit` (可选): 每页数量,默认10 -- `keyword` (可选): 搜索关键词 - -**响应格式**: -```json -{ - "code": 1, - "msg": "获取成功", - "data": { - "list": [ - { - "id": 1, - "title": "少儿体适能训练的核心要素", - "image": "https://example.com/cover.jpg", - "content": "富文本内容", - "table_type": "1", - "category_name": "课程教学大纲", - "type": 1, - "url": "", - "status": 1, - "create_time": 1705294800, - "update_time": 1705294800, - "user_permission": "1,2,3", - "is_read": false, - "is_favorite": false - } - ], - "current_page": 1, - "last_page": 5, - "total": 45, - "per_page": 10 - } -} -``` - -**核心业务逻辑**: -```php -// 1. 权限验证:检查 student_ids 字段是否包含当前学员ID -$where[] = ['student_ids', 'like', "%,{$student_id},%"]; -// 或者使用 FIND_IN_SET 函数 -$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"]; - -// 2. 状态筛选:只查询启用状态的内容 -$where[] = ['status', '=', 1]; - -// 3. 软删除筛选:排除已删除的内容 -$where[] = ['delete_time', '=', 0]; - -// 4. 分类筛选 -if (!empty($category)) { - $where[] = ['table_type', '=', $category]; -} - -// 5. 关键词搜索(只搜索标题和内容) -if (!empty($keyword)) { - $where[] = ['title|content', 'like', "%{$keyword}%"]; -} - -// 6. 查询收藏状态和阅读状态(需要关联查询) -// 7. 分页查询 -// 8. 数据格式化(时间戳转换等) -``` - -#### **1.2 获取知识分类列表接口** -```php -GET /api/student/knowledge/categories -``` - -**响应格式**: -```json -{ - "code": 1, - "msg": "获取成功", - "data": [ - { - "value": "1", - "text": "课程教学大纲", - "icon": "📖", - "count": 12 - }, - { - "value": "2", - "text": "跳绳教案库", - "icon": "🏃", - "count": 8 - } - ] -} -``` - -#### **1.3 获取推荐文章接口** -```php -GET /api/student/knowledge/recommend/{student_id} -``` - -**业务逻辑**: -- 返回热门文章(按阅读量排序) -- 或者返回最新发布的文章 -- 限制返回数量(如5篇) - -### **任务2:知识库交互功能开发** -**负责人**:后端开发者 -**工期**:1天 -**优先级**:中 - -#### **2.1 文章详情接口** -```php -GET /api/student/knowledge/detail/{id} -``` - -**请求参数**: -- `id` (必填): 文章ID -- `student_id` (必填): 学员ID(权限验证) - -**响应格式**: -```json -{ - "code": 1, - "msg": "获取成功", - "data": { - "id": 1, - "title": "少儿体适能训练的核心要素", - "image": "https://example.com/cover.jpg", - "content": "

富文本内容...

", - "table_type": "1", - "category_name": "课程教学大纲", - "type": 1, - "url": "", - "status": 1, - "create_time": 1705294800, - "update_time": 1705294800, - "user_permission": "1,2,3", - "exam_papers_id": "", - "is_read": true, - "is_favorite": false - } -} -``` - -#### **2.2 标记文章已读接口** -```php -POST /api/student/knowledge/mark-read -``` - -**请求参数**: -```json -{ - "article_id": 1, - "student_id": 31 -} -``` - -**业务逻辑**: -- 需要创建阅读记录表或在现有表中添加字段 -- 更新文章的阅读次数 -- 防止重复标记 - -#### **2.3 收藏/取消收藏接口** -```php -POST /api/student/knowledge/toggle-favorite -``` - -**请求参数**: -```json -{ - "article_id": 1, - "student_id": 31, - "action": "add" // add-收藏, remove-取消收藏 -} -``` - -**业务逻辑**: -- 需要创建收藏表 `school_student_favorites` -- 支持收藏和取消收藏操作 -- 返回当前收藏状态 - -### **任务3:统计和搜索功能开发** -**负责人**:后端开发者 -**工期**:0.5天 -**优先级**:低 - -#### **3.1 获取知识库统计接口** -```php -GET /api/student/knowledge/stats/{student_id} -``` - -**响应格式**: -```json -{ - "code": 1, - "msg": "获取成功", - "data": { - "total_articles": 45, - "favorites": 12, - "read_articles": 28, - "categories_count": { - "1": 12, - "2": 8, - "3": 6 - } - } -} -``` - -#### **3.2 搜索文章接口** -```php -GET /api/student/knowledge/search/{student_id} -``` - -**请求参数**: -- `keyword` (必填): 搜索关键词 -- `category` (可选): 分类筛选 -- `page` (可选): 页码 - -**搜索范围**: -- 文章标题(title字段) -- 文章内容(content字段) - -## 🗄️ **数据库设计补充** - -### **需要新增的表** - -#### **1. 学员文章阅读记录表** -```sql -CREATE TABLE `school_student_article_reads` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `student_id` int(11) NOT NULL COMMENT '学员ID', - `article_id` int(11) NOT NULL COMMENT '文章ID', - `read_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间', - `read_duration` int(11) DEFAULT 0 COMMENT '阅读时长(秒)', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_student_article` (`student_id`, `article_id`), - KEY `idx_student_id` (`student_id`), - KEY `idx_article_id` (`article_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员文章阅读记录表'; -``` - -#### **2. 学员收藏表** -```sql -CREATE TABLE `school_student_favorites` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `student_id` int(11) NOT NULL COMMENT '学员ID', - `target_type` varchar(50) NOT NULL COMMENT '收藏类型:article-文章', - `target_id` int(11) NOT NULL COMMENT '目标ID', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_student_target` (`student_id`, `target_type`, `target_id`), - KEY `idx_student_id` (`student_id`), - KEY `idx_target` (`target_type`, `target_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员收藏表'; -``` - -### **现有表字段补充** - -#### **school_lesson_course_teaching 表优化** -```sql --- 注意:school_lesson_course_teaching 表已有完整字段结构 --- 现有字段已足够支持知识库功能,无需添加额外字段 --- 如果需要统计功能,通过关联查询其他表实现 -``` - -## 🔗 **前后端联调任务** - -### **阶段1:基础功能联调** -**负责人**:前端+后端 -**工期**:0.5天 - -#### **联调内容**: -1. **文章列表接口联调** - - 验证分页功能 - - 验证分类筛选 - - 验证权限控制 - - 验证数据格式 - -2. **分类列表接口联调** - - 验证分类数据正确性 - - 验证统计数量准确性 - -3. **推荐文章接口联调** - - 验证推荐算法 - - 验证数据格式 - -#### **测试用例**: -```javascript -// 1. 测试文章列表 -const listResponse = await apiRoute.getKnowledgeArticles({ - student_id: 31, - category: '', - page: 1, - limit: 10 -}); - -// 2. 测试分类筛选 -const categoryResponse = await apiRoute.getKnowledgeArticles({ - student_id: 31, - category: '1', // 课程教学大纲 - page: 1, - limit: 10 -}); - -// 3. 测试权限控制 -const unauthorizedResponse = await apiRoute.getKnowledgeArticles({ - student_id: 999, // 无权限的学员ID - page: 1, - limit: 10 -}); -``` - -### **阶段2:交互功能联调** -**负责人**:前端+后端 -**工期**:0.5天 - -#### **联调内容**: -1. **文章详情接口联调** - - 验证富文本内容渲染 - - 验证权限控制 - - 验证阅读状态 - -2. **阅读标记功能联调** - - 验证阅读记录创建 - - 验证阅读次数更新 - - 验证重复标记处理 - -3. **收藏功能联调** - - 验证收藏/取消收藏 - - 验证收藏状态同步 - - 验证收藏统计更新 - -#### **测试用例**: -```javascript -// 1. 测试文章详情 -const detailResponse = await apiRoute.getKnowledgeDetail({ - id: 1, - student_id: 31 -}); - -// 2. 测试标记已读 -const readResponse = await apiRoute.markArticleRead({ - article_id: 1, - student_id: 31 -}); - -// 3. 测试收藏功能 -const favoriteResponse = await apiRoute.toggleArticleFavorite({ - article_id: 1, - student_id: 31, - action: 'add' -}); -``` - -### **阶段3:搜索和统计功能联调** -**负责人**:前端+后端 -**工期**:0.5天 - -#### **联调内容**: -1. **搜索功能联调** - - 验证关键词搜索 - - 验证搜索结果准确性 - - 验证搜索性能 - -2. **统计功能联调** - - 验证文章总数统计 - - 验证收藏数统计 - - 验证分类统计 - -#### **测试用例**: -```javascript -// 1. 测试搜索功能 -const searchResponse = await apiRoute.searchKnowledgeArticles({ - student_id: 31, - keyword: '体适能', - category: '', - page: 1, - limit: 10 -}); - -// 2. 测试统计功能 -const statsResponse = await apiRoute.getKnowledgeStats({ - student_id: 31 -}); -``` - -## 📊 **开发进度安排** - -### **第1天:基础接口开发** -- 上午:文章列表接口开发 -- 下午:分类列表和推荐文章接口开发 - -### **第2天:交互功能开发** -- 上午:文章详情和阅读标记接口开发 -- 下午:收藏功能接口开发 - -### **第3天:搜索统计和联调** -- 上午:搜索和统计接口开发 -- 下午:前后端联调测试 - -**总工期:2.5天** - -## ✅ **验收标准** - -### **功能验收** -- [ ] 文章列表正确显示,支持分页 -- [ ] 分类筛选功能正常 -- [ ] 权限控制严格(只能看到有权限的文章) -- [ ] 搜索功能准确 -- [ ] 收藏功能正常 -- [ ] 阅读状态正确标记 -- [ ] 统计数据准确 - -### **性能验收** -- [ ] 文章列表加载时间 < 1秒 -- [ ] 搜索响应时间 < 2秒 -- [ ] 富文本内容渲染正常 -- [ ] 图片加载优化 - -### **安全验收** -- [ ] 权限控制严格 -- [ ] 防止SQL注入 -- [ ] 防止XSS攻击 -- [ ] 敏感数据过滤 - -### **兼容性验收** -- [ ] 富文本内容在小程序中正常显示 -- [ ] 图片在不同设备上正常显示 -- [ ] 分页功能在不同网络环境下正常 - -## 🔧 **技术实现要点** - -### **权限控制实现** -```php -// 使用 FIND_IN_SET 函数检查权限(推荐) -$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"]; - -// 或者使用 LIKE 查询(需要确保 student_ids 格式为 ",1,2,3,") -$where[] = ['student_ids', 'like', "%,{$student_id},%"]; - -// 同时确保只查询启用状态的内容 -$where[] = ['status', '=', 1]; -$where[] = ['delete_time', '=', 0]; -``` - -### **富文本内容处理** -```php -// 过滤危险标签 -$content = strip_tags($content, '


'); - -// 处理图片路径(如果需要) -$content = str_replace('src="/', 'src="' . $domain . '/', $content); - -// 处理时间戳格式化 -$create_time_formatted = date('Y-m-d H:i:s', $create_time); -$update_time_formatted = date('Y-m-d H:i:s', $update_time); -``` - -### **搜索优化** -```php -// 使用全文索引提高搜索性能 -ALTER TABLE `school_lesson_course_teaching` -ADD FULLTEXT KEY `ft_title_content` (`title`, `content`); - -// 全文搜索查询 -$where[] = ['', 'exp', "MATCH(title, content) AGAINST('{$keyword}' IN NATURAL LANGUAGE MODE)"]; -``` - -### **缓存策略** -```php -// 分类列表缓存(1小时) -$categories = Cache::remember('knowledge_categories', 3600, function() { - return $this->getCategoriesFromDB(); -}); - -// 推荐文章缓存(30分钟) -$recommend = Cache::remember("knowledge_recommend_{$student_id}", 1800, function() use ($student_id) { - return $this->getRecommendArticles($student_id); -}); -``` - -这个详细的开发任务文档涵盖了知识库模块的完整开发流程,包括后端接口开发、数据库设计、前后端联调和验收标准,可以作为开发团队的具体执行指南。