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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 全部
- 系统消息
- 通知公告
- 作业任务
- 反馈评价
- 课程提醒
- 订单消息
-
-
-
-
-
-
-
-
-
- {{ getMessageTypeText(row.message_type) }}
-
-
-
-
-
-
-
-
- {{ row.content.length > 50 ? row.content.substring(0, 50) + '...' : row.content }}
-
-
-
-
-
-
- {{ row.is_read ? '已读' : '未读' }}
-
-
-
-
-
-
-
-
- {{ row.read_time || '未读' }}
-
-
-
-
-
-
- 查看
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ getMessageTypeText(selectedMessage.message_type) }}
-
-
-
- {{ selectedMessage.created_at }}
-
-
-
- {{ selectedMessage.is_read ? '已读' : '未读' }}
-
-
-
- {{ selectedMessage.read_time || '未读' }}
-
-
- {{ selectedMessage.title }}
-
-
-
-
-
消息内容:
-
- {{ selectedMessage.content }}
-
-
-
-
-
-
-
-
-
-
-```
-
-## 🔧 **数据库优化方案**
-
-### **✅ 核心表结构已完善**
-
-经过验证,`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);
-});
-```
-
-这个详细的开发任务文档涵盖了知识库模块的完整开发流程,包括后端接口开发、数据库设计、前后端联调和验收标准,可以作为开发团队的具体执行指南。