Browse Source

临时提交

master
王泽彦 8 months ago
parent
commit
2ddc7305cd
  1. 198
      SCHEDULE_FIXES_SUMMARY.md
  2. 18
      admin/README.md
  3. 266
      admin/Word模板解析填充功能开发计划.md
  4. 12
      admin/src/app/views/document-template.vue
  5. 79
      gift_table_documentation.md
  6. 56
      niucloud/README.md
  7. 6
      niucloud/app/api/controller/apiController/Course.php
  8. 31
      niucloud/app/api/controller/apiController/StudentCourse.php
  9. 2
      niucloud/app/api/route/route.php
  10. 4
      niucloud/app/service/api/apiService/CourseService.php
  11. 2
      niucloud/app/service/api/apiService/CustomerResourcesService.php
  12. 112
      niucloud/app/service/api/apiService/ServiceService.php
  13. 173
      niucloud/课程安排资料.md
  14. 3
      uniapp/README.md
  15. 19
      uniapp/api/apiRoute.js
  16. 30
      uniapp/components/order-list-card/index.vue
  17. 179
      uniapp/components/service-list-card/index.vue
  18. 285
      uniapp/cross-platform-compatibility-report.md
  19. 311
      uniapp/pages/market/clue/class_arrangement_detail.vue
  20. 68
      uniapp/pages/market/clue/clue_info.vue
  21. 234
      uniapp/student-functionality-validation.md
  22. 280
      uniapp权限管理配置文档.md

198
SCHEDULE_FIXES_SUMMARY.md

@ -1,198 +0,0 @@
# 定时任务修复总结
## 修复概述
本次修复主要解决了系统中4个定时任务的关键逻辑问题和性能问题,并创建了统一的执行锁机制基类。
## 修复的问题
### 1. 资源自动分配逻辑问题 ✅
**文件**: `niucloud/app/job/transfer/schedule/ResourceAutoAllocation.php`
**问题**:
- 资源分配时创建新记录而不是更新现有记录,导致重复分配
- 缺乏执行锁机制,可能并发执行
**修复**:
- 改为更新现有记录而非创建新记录
- 添加执行锁机制(5分钟锁定时间)
- 改进分配统计和日志记录
- 继承统一的 `BaseScheduleJob` 基类
### 2. 自动排课重复执行问题 ✅
**文件**: `niucloud/app/job/transfer/schedule/CourseScheduleJob.php`
**问题**:
- 每天执行时重复创建未来30天的课程
- 缺乏重复执行控制机制
**修复**:
- 添加执行锁机制(10分钟锁定时间)
- 添加每日执行标记,防止重复执行
- 改进模板数据获取逻辑,使用最近的有效数据作为模板
- 优化日志记录和结果返回
### 3. 绩效计算重复和事务问题 ✅
**文件**: `niucloud/app/job/transfer/schedule/PerformanceCalculation.php`
**问题**:
- `performanceService` 初始化为 null 导致调用失败
- 多人介入订单处理逻辑混乱
- 缺乏完整的重复检查机制
- 事务处理不完善
**修复**:
- 修复 `performanceService` 初始化问题
- 添加执行锁机制(30分钟锁定时间)
- 重构保存逻辑,按订单分组处理避免重复更新
- 完善事务处理和错误处理
- 改进统计和日志记录
### 4. 课程状态批量更新性能问题 ✅
**文件**: `niucloud/app/job/schedule/HandleCourseSchedule.php`
**问题**:
- 在循环中逐条更新,性能极差
- 缺乏事务保护
- 可能重复更新相同记录
**修复**:
- 改为批量更新操作
- 添加事务保护
- 添加执行锁机制(5分钟锁定时间)
- 避免重复更新已完成的课程
- 改进错误处理和统计
### 5. 统一执行锁机制 ✅
**文件**: `niucloud/core/base/BaseScheduleJob.php`
**新增功能**:
- 创建定时任务基类 `BaseScheduleJob`
- 提供统一的执行锁机制
- 支持每日执行标记
- 统一的日志记录格式
- 自动清理过期标记文件
- 标准化的结果返回格式
## 修复效果
### 性能提升
- **课程状态更新**: 从逐条更新改为批量更新,性能提升约90%
- **绩效计算**: 优化事务处理和重复检查,减少数据库锁定时间
- **资源分配**: 改进分配逻辑,避免重复记录创建
### 数据一致性
- 所有定时任务都添加了事务保护
- 完善的错误处理和回滚机制
- 避免重复执行和数据重复
### 系统稳定性
- 统一的执行锁机制防止任务冲突
- 每日执行标记避免重复处理
- 完善的异常处理和日志记录
## 技术改进
### 1. 执行锁机制
- 文件锁防止任务重复执行
- 可配置的锁定时间
- 自动清理机制
### 2. 每日执行控制
- 标记文件防止重复执行
- 自动清理过期标记
- 适用于每日执行一次的任务
### 3. 统一基类设计
- `BaseScheduleJob` 提供通用功能
- 子类只需实现具体业务逻辑
- 标准化的错误处理和日志
### 4. 改进的事务处理
- 合理的事务边界
- 完善的回滚机制
- 按业务单元分组处理
## 使用方法
### 启动定时任务服务
```bash
# 启动定时任务服务
docker exec niucloud_php php think cron:schedule start
# 检查服务状态
docker exec niucloud_php php think cron:schedule status
# 停止服务
docker exec niucloud_php php think cron:schedule stop
```
### 手动执行单个任务
```bash
# 测试资源分配任务
docker exec niucloud_php php think queue:work --queue resource_auto_allocation
# 测试自动排课任务
docker exec niucloud_php php think queue:work --queue course_schedule_job
```
### 创建新的定时任务
继承 `BaseScheduleJob` 基类:
```php
<?php
namespace app\job\example;
use core\base\BaseScheduleJob;
class ExampleJob extends BaseScheduleJob
{
protected $jobName = 'example_job';
protected $lockTimeout = 300; // 5分钟
protected $enableDailyFlag = true; // 启用每日执行标记
protected function executeJob()
{
// 实现具体业务逻辑
return $this->getSuccessResult(['processed' => 100]);
}
}
```
## 监控和维护
### 日志文件位置
- 定时任务日志: `niucloud/runtime/log/`
- 锁文件位置: `niucloud/runtime/`
- 执行标记: `niucloud/runtime/`
### 关键监控指标
- 任务执行时间
- 成功/失败率
- 数据处理量
- 锁文件状态
### 故障排查
1. 检查日志文件中的错误信息
2. 查看锁文件是否存在异常
3. 验证数据库连接和权限
4. 检查系统资源使用情况
## 后续建议
1. **添加监控告警**: 对关键任务失败进行告警
2. **性能监控**: 记录任务执行时间和资源使用
3. **数据备份**: 在重要操作前添加数据备份
4. **测试环境验证**: 定期在测试环境验证任务逻辑
5. **文档更新**: 保持文档与代码同步更新
## 风险控制
所有修复都已通过以下验证:
- ✅ PHP语法检查通过
- ✅ 数据库操作事务完整
- ✅ 错误处理机制完善
- ✅ 日志记录详细
- ✅ 向后兼容性保持
修复后的定时任务更加稳定、高效,并且具有更好的可维护性。

18
admin/README.md

@ -1,18 +0,0 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

266
admin/Word模板解析填充功能开发计划.md

@ -1,266 +0,0 @@
# Word模板解析填充功能开发计划
## 需求分析与问题识别
### 📋 核心需求
- 解析Word模板中的占位符(如:`{{学员姓名}}`、`{{签约日期}}`等)
- 提供可视化配置界面,让用户选择字段数据源
- 支持数据库字段映射和自定义函数处理
- 生成填充后的Word文档
### ⚠️ 需求存在的潜在问题及建议
#### 1. 技术实现复杂性问题
**问题描述:**
- Word .doc格式是二进制格式,解析困难
- PHP直接处理Word文档需要复杂的第三方库
- 占位符格式需要统一标准
**✅ 确认方案:**
- 统一使用 `.docx` 格式(基于XML,更易解析)
- 采用 `phpoffice/phpword` 库处理文档
- 制定标准占位符格式:`{{字段名}}`
#### 2. 数据安全风险
**问题描述:**
- 用户可输入任意表名和字段名存在SQL注入风险
- 函数名直接执行存在代码注入风险
**✅ 确认方案:**
- 限制可访问的数据表白名单
- 预定义可用函数列表,不允许动态执行
- 添加数据访问权限验证
#### 3. 性能考虑
**问题描述:**
- 大量模板同时处理可能导致内存溢出
- 文档生成可能耗时较长
**✅ 确认方案:**
- 实现异步文档生成队列
- 添加文档缓存机制
- 限制并发处理数量
## 技术架构设计
### 数据库设计
#### 1. school_contract合同表
```sql
CREATE TABLE `school_contract` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '合同编号',
`contract_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同名称',
`contract_template` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同模板',
`contract_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同状态',
`contract_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同类型',
`remarks` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '合同备注',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '逻辑删除时间',
`placeholder` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '占位符配置用 json 数据来存储',
PRIMARY KEY (`id`) USING BTREE
)
```
**placeholder字段JSON结构示例:**
```json
{
"{{学员姓名}}": {
"name": "学员姓名",
"data_source": "database",
"table_name": "school_student",
"field_name": "student_name",
"process_function": null,
"default_value": "",
"is_required": true
},
"{{签约日期}}": {
"name": "签约日期",
"data_source": "manual",
"table_name": null,
"field_name": null,
"process_function": "formatDate",
"default_value": "",
"is_required": true
}
}
```
#### 2. 合同和人员关系表 `school_document_generate_log`
```sql
CREATE TABLE `school_document_generate_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`site_id` int(11) NOT NULL DEFAULT '0',
`template_id` int(11) NOT NULL COMMENT '模板ID',
`user_id` int(11) NOT NULL COMMENT '操作用户',
`fill_data` text COMMENT '填充数据JSON',
`generated_file` varchar(500) DEFAULT NULL COMMENT '生成文件路径',
`status` enum('pending','processing','completed','failed') NOT NULL DEFAULT 'pending',
`error_msg` text COMMENT '错误信息',
`created_at` int(11) NOT NULL DEFAULT '0',
`completed_at` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档生成记录表';
```
### 前端页面架构
#### 页面结构
```
admin/src/app/views/document-template/
├── index.vue # 模板列表页
├── add.vue # 新增/编辑模板页
├── config.vue # 占位符配置页
├── generate.vue # 文档生成页
└── components/
├── PlaceholderConfig.vue # 占位符配置组件
├── DataSourceSelector.vue # 数据源选择组件
└── DocumentPreview.vue # 文档预览组件
```
#### 核心交互流程
1. **模板上传** → 自动解析占位符 → 显示配置界面
2. **占位符配置** → 选择数据源 → 设置处理函数 → 保存配置
3. **文档生成** → 选择模板 → 填写/选择数据 → 生成文档 → 下载
### 后端API设计
#### Controller层
```php
// app/adminapi/controller/document/DocumentTemplate.php
class DocumentTemplate extends BaseAdminController
{
public function getPage() // 获取模板列表
public function add() // 新增模板
public function edit() // 编辑模板
public function delete() // 删除模板
public function uploadTemplate() // 上传模板文件
public function parsePlaceholder() // 解析占位符
public function configPlaceholder() // 配置占位符
public function generateDocument() // 生成文档
public function getGenerateLog() // 获取生成记录
}
```
#### Service层核心方法
```php
class DocumentTemplateService
{
public function parseWordTemplate($filePath) // 解析Word模板
public function extractPlaceholders($content) // 提取占位符
public function validateDataSource($config) // 验证数据源配置
public function fillTemplate($templateId, $data) // 填充模板数据
public function generateDocument($config) // 生成最终文档
}
```
## 开发任务分解
### 阶段一:基础架构搭建(2天)
- [✅] 数据库表设计和创建
- [ ] 后端基础Controller和Service创建
- [ ] 前端页面路由和基础组件搭建
- [ ] 文件上传功能集成
### 阶段二:Word解析功能(3天)
- [ ] 集成phpoffice/phpword库
- [ ] 实现Word文档读取和占位符提取
- [ ] 开发占位符正则匹配算法
- [ ] 实现模板预览功能
### 阶段三:配置管理功能(2天)
- [ ] 占位符配置界面开发
- [ ] 数据源选择组件开发
- [ ] 数据表和字段动态获取API
- [ ] 处理函数管理功能
### 阶段四:文档生成功能(3天)
- [ ] 数据填充逻辑实现
- [ ] Word文档生成功能
- [ ] 文件下载和预览功能
- [ ] 异步生成队列实现
### 阶段五:测试和优化(1天)
- [ ] 功能测试和Bug修复
- [ ] 性能优化
- [ ] 安全性检查
- [ ] 用户体验优化
## 技术选型
### 后端依赖
- `phpoffice/phpword`: Word文档处理
- `phpoffice/common`: 公共组件
- PHP 7.4+ (项目要求)
### 前端依赖
- Vue 3 + Composition API
- Element Plus UI组件库
- Axios HTTP客户端
- 文件上传组件
## 风险评估与应对
### 高风险项
1. **Word格式兼容性**
- 风险:不同版本Word文档解析失败
- 应对:支持多种格式,提供格式转换工具
2. **性能问题**
- 风险:大文档处理缓慢
- 应对:异步处理+进度条+缓存机制
3. **安全风险**
- 风险:SQL注入、代码注入
- 应对:严格的输入验证和白名单机制
### 中风险项
1. **用户体验复杂度**
- 风险:配置过程过于复杂
- 应对:提供模板和向导式配置
2. **数据一致性**
- 风险:配置与实际数据不匹配
- 应对:实时验证和错误提示
## 测试策略
### 单元测试
- 占位符解析算法测试
- 数据填充逻辑测试
- 文档生成功能测试
### 集成测试
- 完整流程端到端测试
- 不同格式文档兼容性测试
- 并发处理压力测试
### 安全测试
- SQL注入防护测试
- 文件上传安全测试
- 权限控制测试
## 上线计划
### 灰度发布
1. 内部测试环境验证
2. 小范围用户试用
3. 收集反馈优化
4. 全量发布
### 监控指标
- 文档生成成功率
- 处理时间监控
- 错误日志分析
- 用户使用频率统计
---
## 总结
这个功能具有一定复杂性,建议分阶段实施。核心关注点:
1. **安全性**:严格的输入验证和权限控制
2. **性能**:异步处理和缓存优化
3. **易用性**:简化配置流程,提供良好的用户体验
4. **兼容性**:支持多种Word格式和版本
请确认以上计划是否符合您的期望,如有调整需求请告知。确认后我们开始具体的开发实施。

12
admin/src/app/views/document-template.vue

@ -0,0 +1,12 @@
<!--
Word模板管理页面 - 路由入口文件
此文件是为了匹配动态路由系统的组件加载规则而创建的
实际功能组件位于: ./document-template/index.vue
-->
<template>
<DocumentTemplateIndex />
</template>
<script setup lang="ts">
import DocumentTemplateIndex from './document-template/index.vue'
</script>

79
gift_table_documentation.md

@ -1,79 +0,0 @@
# 赠品表设计文档
## 表结构说明
### 表名:`gift`
**用途**:存储系统中的赠品信息,包括赠课和代金券等赠品类型
### 字段说明
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `id` | int(11) | AUTO_INCREMENT | 赠品主键ID |
| `gift_name` | varchar(255) | '' | 赠品名称 |
| `gift_type` | varchar(50) | '' | 赠品类型:course(赠课),voucher(代金券) |
| `gift_time` | int(11) | 0 | 赠送时间(时间戳) |
| `giver_id` | int(11) | 0 | 赠送来源人员ID |
| `resource_id` | int(11) | 0 | 赠品归属资源ID |
| `order_id` | int(11) | 0 | 赠品使用的订单ID(0表示未使用) |
| `gift_status` | tinyint(4) | 1 | 赠品状态:1=未使用,2=已使用,3=已过期,4=已作废 |
| `use_time` | int(11) | 0 | 赠品使用时间(时间戳) |
| `create_time` | int(11) | 0 | 创建时间(时间戳) |
| `update_time` | int(11) | 0 | 更新时间(时间戳) |
| `delete_time` | int(11) | 0 | 删除时间(时间戳,0表示未删除) |
### 索引设计
#### 主键索引
- `PRIMARY KEY (id)` - 主键索引,自动创建
#### 普通索引
1. `IDX_gift_giver_id` - 赠送人员索引
- **用途**:根据赠送人员ID查询赠品
- **场景**:统计某个人员赠送的所有赠品
2. `IDX_gift_resource_id` - 资源ID索引
- **用途**:根据资源ID查询相关赠品
- **场景**:查询某个资源相关的所有赠品
3. `IDX_gift_order_id` - 订单ID索引
- **用途**:根据订单ID查询使用的赠品
- **场景**:查询某个订单中使用了哪些赠品
4. `IDX_gift_status` - 状态索引
- **用途**:根据赠品状态快速筛选
- **场景**:查询未使用、已使用、已过期等状态的赠品
5. `IDX_gift_type` - 类型索引
- **用途**:根据赠品类型快速筛选
- **场景**:分别查询赠课或代金券
6. `IDX_gift_time` - 赠送时间索引
- **用途**:根据赠送时间范围查询
- **场景**:统计某个时间段的赠品发放情况
### 业务逻辑说明
#### 赠品状态流转
1. **未使用(1)****已使用(2)**:用户使用赠品时更新
2. **未使用(1)****已过期(3)**:系统定时任务检查过期
3. **未使用(1)****已作废(4)**:管理员手动作废
4. **已使用(2)**:终态,不可再变更
#### 关键业务场景
1. **赠品发放**:创建记录,设置 `gift_status=1`
2. **赠品使用**:更新 `order_id`、`use_time`、`gift_status=2`
3. **赠品过期**:定时任务更新 `gift_status=3`
4. **赠品作废**:管理员操作更新 `gift_status=4`
### 性能优化建议
1. **复合索引考虑**
- 如果经常按状态+类型查询,可考虑创建复合索引 `(gift_status, gift_type)`
- 如果经常按时间范围+状态查询,可考虑创建复合索引 `(gift_time, gift_status)`
2. **分区表考虑**
- 如果数据量很大,可考虑按时间分区
3. **归档策略**
- 定期归档已删除或过期的赠品数据

56
niucloud/README.md

@ -1,56 +0,0 @@
ThinkPHP 6.0
===============
> 运行环境要求PHP7.2+,兼容PHP8.1
[官方应用服务市场](https://market.topthink.com) | [`ThinkAPI`——官方统一API服务](https://docs.topthink.com/think-api)
ThinkPHPV6.0版本由[亿速云](https://www.yisu.com/)独家赞助发布。
## 主要新特性
* 采用`PHP7`强类型(严格模式)
* 支持更多的`PSR`规范
* 原生多应用支持
* 更强大和易用的查询
* 全新的事件系统
* 模型事件和数据库事件统一纳入事件系统
* 模板引擎分离出核心
* 内部功能中间件化
* SESSION/Cookie机制改进
* 对Swoole以及协程支持改进
* 对IDE更加友好
* 统一和精简大量用法
## 安装
~~~
composer create-project topthink/think tp 6.0.*
~~~
如果需要更新框架使用
~~~
composer update topthink/framework
~~~
## 文档
[完全开发手册](https://www.kancloud.cn/manual/thinkphp6_0/content)
## 参与开发
请参阅 [ThinkPHP 核心框架包](https://github.com/top-think/framework)。
## 版权信息
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
本项目包含的第三方源码和二进制文件之版权信息另行标注。
版权所有Copyright © 2006-2021 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
更多细节参阅 [LICENSE.txt](LICENSE.txt)

6
niucloud/app/api/controller/apiController/Course.php

@ -107,7 +107,11 @@ class Course extends BaseApiService
["person_type",''],
["schedule_id",''],
["course_date",''],
["time_slot",'']
["time_slot",''],
["schedule_type", 1], // 1=正式位, 2=等待位
["course_type", 1], // 1=正式课, 2=体验课, 3=等待位
["position", ''], // 位置信息
["remark", ''] // 备注
]);
return (new CourseService())->addSchedule($data);
}

31
niucloud/app/api/controller/apiController/StudentCourse.php

@ -13,6 +13,7 @@ namespace app\api\controller\apiController;
use app\Request;
use app\service\api\apiService\StudentCourseService;
use app\service\api\apiService\ServiceService;
use core\base\BaseApiService;
/**
@ -64,6 +65,36 @@ class StudentCourse extends BaseApiService
}
}
/**
* 获取学员服务记录
* @param Request $request
* @return \think\Response
*/
public function getServiceList(Request $request)
{
$student_id = $request->param('student_id', '');
// 如果没有传student_id,尝试从当前登录用户获取
if (empty($student_id)) {
$student_id = $this->getResourceIdByMemberId($this->member_id);
}
if (empty($student_id)) {
return fail('学员ID不能为空');
}
try {
$res = (new ServiceService())->getStudentServiceList((int)$student_id);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
return fail('获取服务记录失败:' . $e->getMessage());
}
}
/**
* 根据会员ID获取资源ID
* @param int $member_id

2
niucloud/app/api/route/route.php

@ -524,6 +524,8 @@ Route::group(function () {
//学生端-课程详情
Route::get('xy/course/detail', 'apiController.StudentCourse/courseDetail');
//学生端-服务记录列表
Route::get('xy/service/list', 'apiController.StudentCourse/getServiceList');
/***************************************************** 字典批量获取 ****************************************************/
// 批量获取字典数据

4
niucloud/app/service/api/apiService/CourseService.php

@ -445,6 +445,10 @@ class CourseService extends BaseApiService
'schedule_id' => $data['schedule_id'],
'course_date' => $data['course_date'],
'time_slot' => $data['time_slot'],
'schedule_type' => $data['schedule_type'] ?? 1, // 1=正式位, 2=等待位
'course_type' => $data['course_type'] ?? 1, // 1=正式课, 2=体验课, 3=等待位
'position' => $data['position'] ?? '', // 位置信息
'remark' => $data['remark'] ?? '' // 备注
]);
$CourseSchedule->where(['id' => $data['schedule_id']])->dec("available_capacity")->update();
return success("添加成功");

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

@ -89,7 +89,7 @@ class CustomerResourcesService extends BaseApiService
$model = $model->where('name', 'like', "%{$where['name']}%");
}
if(!empty($where['phone_number'])){
$model = $model->where('phone_number', $where['phone_number']);
$model = $model->where('phone_number', 'like', "%{$where['phone_number']}%");
}
$data = $model->field($field)
->with([

112
niucloud/app/service/api/apiService/ServiceService.php

@ -157,4 +157,116 @@ class ServiceService extends BaseApiService
return ['code' => 0, 'msg' => '更新失败:' . $e->getMessage()];
}
}
/**
* 获取学员服务记录列表
* @param int $studentId 学员ID
* @return array
*/
public function getStudentServiceList(int $studentId)
{
try {
// 查询SQL:关联服务基础信息、课程信息、员工信息
$list = $this->model->alias('sl')
->join('school_service s', 'sl.service_id = s.id')
->leftJoin('school_course c', 'sl.course_id = c.id')
->leftJoin('school_personnel p', 'sl.staff_id = p.id')
->where('sl.resource_id', $studentId)
->where('s.deleted_at', 0)
->field([
// 服务基础信息
's.id as service_id',
's.service_name',
's.preview_image_url',
's.description',
's.service_type',
's.status as service_status',
// 服务记录信息
'sl.id as log_id',
'sl.status as log_status',
'sl.score',
'sl.feedback',
'sl.service_remark',
'sl.created_at as service_time',
'sl.feedback_time',
'sl.updated_at',
// 课程信息
'c.course_name',
// 员工信息
'p.name as staff_name'
])
->order('sl.created_at desc, sl.status asc')
->select()
->toArray();
// 处理数据格式,按服务分组
$serviceMap = [];
foreach ($list as $item) {
$serviceId = $item['service_id'];
if (!isset($serviceMap[$serviceId])) {
// 初始化服务信息
$serviceMap[$serviceId] = [
'id' => $serviceId,
'service_name' => $item['service_name'],
'preview_image_url' => $item['preview_image_url'],
'description' => $item['description'],
'service_type' => $item['service_type'],
'status' => $this->mapServiceStatus($item['service_status']),
'logs' => [],
'total_count' => 0,
'completed_count' => 0
];
}
// 添加服务记录
if ($item['log_id']) {
$log = [
'id' => $item['log_id'],
'status' => $item['log_status'],
'service_content' => $item['service_remark'],
'service_staff' => $item['staff_name'],
'service_time' => $item['service_time'] ?: '',
'duration' => $item['updated_at'] ?: '',
'customer_feedback' => $item['feedback'],
'service_rating' => $item['score'] && $item['score'] <= 5 ? $item['score'] : null,
'remark' => $item['service_remark'],
'course_name' => $item['course_name'],
'updated_at' => $item['updated_at'] ?: ''
];
$serviceMap[$serviceId]['logs'][] = $log;
$serviceMap[$serviceId]['total_count']++;
if ($item['log_status'] == 1) {
$serviceMap[$serviceId]['completed_count']++;
}
}
}
return ['code' => 1, 'data' => array_values($serviceMap), 'msg' => '获取成功'];
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '获取失败:' . $e->getMessage()];
}
}
/**
* 映射服务状态
* @param int $status
* @return string
*/
private function mapServiceStatus($status)
{
switch ($status) {
case 1:
return 'active';
case 0:
return 'expired';
case 2:
return 'suspended';
default:
return 'expired';
}
}
}

173
niucloud/课程安排资料.md

@ -1,173 +0,0 @@
school_campus(校区表)
CREATE TABLE `school_campus` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`campus_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区名称',
`campus_address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区地址',
`campus_preview_image` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区预览图,存储图片路径',
`campus_coordinates` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区坐标,格式为经度,纬度',
`campus_introduction` text COLLATE utf8mb4_general_ci COMMENT '校区介绍',
`campus_status` tinyint DEFAULT '1' COMMENT '校区状态:0-禁用,1-启用',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '校区创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '校区更新时间',
`delete_time` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段,0表示未删除,非空表示已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='校区表';
做为基础数据,下面一级就是场地。
school_venue(场地表)
CREATE TABLE `school_venue` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '场地编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`venue_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '场地名称',
`capacity` int NOT NULL COMMENT '场地可容纳人数上限',
`availability_status` tinyint(1) NOT NULL COMMENT '场地可用状态: 1-可用, 0-不可用',
`time_range_type` enum('range','fixed','all') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '场地可用时间范围类型: range-范围类型, fixed-固定时间范围类型',
`time_range_start` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '范围类型的开始时间',
`time_range_end` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '范围类型的结束时间',
`fixed_time_ranges` json DEFAULT NULL COMMENT '固定时间范围类型的可用时间, 存储为JSON数组',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='场地表';
场地表做为课程的容器,主要是用来限制人数,时间,和可用状态。如果场地被设置为不可用,那么该场地下的所有课程都会被取消。其中time_range_type字段决定这个时间范围是范围类型还是固定时间类型。
这个字段配合time_range_start,time_range_end,fixed_time_ranges这三个字段,可以确定这个场地可用的时间范围。
school_course(课程表)
CREATE TABLE `school_course` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '课程编号',
`course_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程名称',
`course_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程类型',
`duration` int NOT NULL COMMENT '课程时长',
`session_count` int NOT NULL COMMENT '课时数量',
`single_session_count` int NOT NULL DEFAULT '0' COMMENT '单次消课数量',
`gift_session_count` int NOT NULL DEFAULT '0' COMMENT '赠送课时数量',
`price` decimal(10,2) NOT NULL COMMENT '课程价格',
`internal_reminder` int NOT NULL COMMENT '内部提醒课时',
`customer_reminder` int NOT NULL COMMENT '客户提醒课时',
`remarks` text COLLATE utf8mb4_general_ci COMMENT '课程备注',
`created_at` int DEFAULT '0' COMMENT '创建时间',
`updated_at` int DEFAULT '0' COMMENT '更新时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`contract_id` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='课程表';
课程表主要是用户上课的内容,在排课阶段主要是看用户排的是什么课在哪个课程安排里展示。课程表和课程安排表是一对多的关系,一个课程可以有多个课程安排。但是在一个周期范围内,这个安排的数量始终是不会超过课程表的课时数量的。
举例来说single_session_count字段表示一次课程安排要消耗多少课时,如果值为 2,session_count课时数量是 10,那么这次课程周期一个用户只能排 5 次课程安排。
school_course_schedule(课程安排表)
CREATE TABLE `school_course_schedule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '课程安排编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`venue_id` int NOT NULL COMMENT '场地ID',
`course_date` date NOT NULL COMMENT '上课日期',
`time_slot` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '上课时段',
`course_id` int NOT NULL COMMENT '课程ID',
`coach_id` int NOT NULL COMMENT '上课教练ID',
`participants` json DEFAULT NULL COMMENT '参与人员列表,存储为JSON数组,包含学员ID和来源信息',
`student_ids` json DEFAULT NULL COMMENT '上课学生ID列表,存储为JSON数组',
`available_capacity` int DEFAULT NULL COMMENT '根据场地容量判断的可安排学员位置数量',
`status` enum('pending','upcoming','ongoing','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'pending' COMMENT '课程状态: pending-待开始, upcoming-即将开始, ongoing-进行中, completed-已结束',
`auto_schedule` tinyint(1) DEFAULT NULL COMMENT '是否自动排课1是0否',
`created_by` enum('manual','system') COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程安排创建方式: manual-人员安排, system-系统创建',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`assistant_ids` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '助教ID列表(逗号分隔)',
`education_id` int DEFAULT NULL COMMENT '教务ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='课程安排表';
这个表是在学校层面针对教室的情况做的课程安排情况表。这个表和课程表是一对多的关系,一个课程可以有多个课程安排。而且支持一次配置后自动排课的功能。这个表的主要作用是记录课程安排的情况,包括上课日期,时间段,教练,参与人员等信息。
school_person_course_schedule(人员与课程安排关系表)
CREATE TABLE `school_person_course_schedule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '关系编号',
`resources_id` int DEFAULT NULL COMMENT '资源ID',
`person_id` int DEFAULT NULL COMMENT '人员ID',
`student_id` int DEFAULT NULL COMMENT '学员ID',
`person_type` enum('student','customer_resource') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '人员类型: student-正式学员, customer_resource-客户资源,teacher教练',
`schedule_id` int NOT NULL COMMENT '课程安排ID',
`course_date` date NOT NULL COMMENT '上课日期',
`schedule_type` tinyint(1) DEFAULT NULL COMMENT '课程安排类型1临时课2固定课',
`course_type` tinyint(1) DEFAULT NULL COMMENT '课程类型1加课2补课3 等待位',
`time_slot` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '上课时段',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` int DEFAULT '0' COMMENT '删除',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态0待上课1已上课2请假',
`remark` varchar(512) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请假备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='人员与课程安排关系表';
这个表记录着学员和school_course_schedule中安排的关系。这里面有几种情况 1.正式的付费学员上课(临时的)2.临时的体验学员上课 3.正式的付费学员固定这个时段上课的。
然后这个课程安排的属性还有是正常消耗课时的,还是补课、还是送课。还是这个教室没有位置单纯是等待的课程。
school_student_courses(学员课程表)
CREATE TABLE `school_student_courses` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '记录编号',
`student_id` int NOT NULL COMMENT '学员ID',
`course_id` int NOT NULL COMMENT '课程ID',
`total_hours` int NOT NULL COMMENT '总正式课时数',
`gift_hours` int DEFAULT '0' COMMENT '赠送课时数',
`start_date` date NOT NULL COMMENT '课程开始日期',
`end_date` date NOT NULL COMMENT '课程结束日期',
`use_total_hours` int NOT NULL DEFAULT '0' COMMENT '已使用课包课时数',
`use_gift_hours` int NOT NULL DEFAULT '0' COMMENT '已使用课包赠送课时数',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`single_session_count` int DEFAULT NULL COMMENT '单次消课数量',
`status` tinyint(1) DEFAULT NULL COMMENT '课程状态1有效2过期3等待期4延期',
`resource_id` int DEFAULT NULL COMMENT '资源ID',
`main_coach_id` int DEFAULT NULL COMMENT '主教练ID',
`assistant_ids` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '助教ID列表(逗号分隔)',
`education_id` int DEFAULT NULL COMMENT '教务ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学员课程表';
这个是一个学员在一个课程周期里可以上课的课时记录以及课时有效期记录,在这个周期范围内为这个学员服务的教练和助教信息。这个表的主要作用是记录学员的课程信息,包括总课时数,赠送课时数,开始和结束日期等信息。这个表和人员与课程安排关系表是一对多的关系,一个学员可以有多个课程安排。
school_student_course_usage(学员课程使用记录表)
CREATE TABLE `school_student_course_usage` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '记录编号',
`student_course_id` int NOT NULL COMMENT '学员课程ID(关联到student_courses表)',
`used_hours` int NOT NULL COMMENT '本次使用的课时数',
`usage_date` date NOT NULL COMMENT '课时使用日期',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学员课时消费记录表';
这个表记录了学员在某个课程周期内使用的课时数,这个表和学员课程表是一对多的关系,一个学员可以有多个课时使用记录。
school_class(班级表)
CREATE TABLE `school_class` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '班级编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`campus_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区名称',
`class_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级名称',
`head_coach` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班继续
级主教练',
`age_group` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级授课年龄段',
`class_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级类型',
`assistant_coach` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级助教',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级状态(1开启 2关闭)',
`sort_order` int NOT NULL COMMENT '班级排序',
`remarks` text COLLATE utf8mb4_general_ci COMMENT '班级备注',
`educational_id` int NOT NULL DEFAULT '0' COMMENT '教务id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='班级表';
学员报名时会分配班级,默认班级里的主教练和助教会被分配到学员课程表里。班级表主要是记录班级的信息,包括班级名称,主教练,助教,授课年龄段等信息。这个表和学员课程表是一对多的关系,一个班级可以有多个学员。
school_class_resources_rel(班级学员关系表)
CREATE TABLE `school_class_resources_rel` (
`id` int NOT NULL AUTO_INCREMENT,
`class_id` int NOT NULL COMMENT '班级id',
`resource_id` int DEFAULT NULL COMMENT '资源id',
`campus_id` int DEFAULT NULL COMMENT '校区id',
`source_id` int DEFAULT NULL COMMENT '数据id',
`source_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据资源类型student是学员,temporary是非正式学员',
`join_time` int DEFAULT NULL COMMENT '加入时间',
`out_time` int DEFAULT NULL COMMENT '离开时间',
`status` tinyint DEFAULT NULL COMMENT '状态1新入2续费3过期4转班5转校',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='班级和资源关系表';
这个表记录了班级和资源的关系,包括班级id,资源id,校区id,数据id,数据资源类型,加入时间,离开时间,状态等信息。这个表和班级表是一对多的关系,一个班级可以有多个资源。

3
uniapp/README.md

@ -1,3 +0,0 @@
# ZhiHuiJiaoWu_UniApp
智慧教务系统UniApp前端项目

19
uniapp/api/apiRoute.js

@ -373,6 +373,10 @@ export default {
async courseInfo(data = {}) {
return await http.get('/course/courseInfo', data);
},
//获取课程安排详情(新接口)
async getScheduleDetail(data = {}) {
return await http.get('/course/scheduleDetail', data);
},
//教研管理文章列表
async teachingResearchList(data = {}) {
return await http.get('/teachingResearch/list', data);
@ -411,6 +415,16 @@ export default {
async xs_getAllCustomerResources(data = {}) {
return await http.get('/customerResources/getAll', data);
},
//客户资源全部列表(用于学员搜索)
async getCustomerResourcesAll(data = {}) {
return await http.get('/customerResources/getAll', data);
},
//获取客户资源详情
async getCustomerResourcesInfo(data = {}) {
return await http.get('/resourceSharing/info', data);
},
//销售端-客户资源-获取修改日志列表
async xs_customerResourcesGetEditLogList(data = {}) {
return await http.get('/customerResources/getEditLogList', data);
@ -513,6 +527,11 @@ export default {
async parent_getChildServices(data = {}) {
return await http.get('/parent/child/services', data);
},
// 获取学员服务记录列表
async getStudentServiceList(data = {}) {
return await http.get('/xy/service/list', data);
},
// 获取指定孩子的消息记录
async parent_getChildMessages(data = {}) {
return await http.get('/parent/child/messages', data);

30
uniapp/components/order-list-card/index.vue

@ -20,9 +20,10 @@
<view
class="order-item"
v-for="(order, index) in orderList"
:key="order.id || index"
:class="{ 'pending-payment': order.status === 'pending' }"
@click="handleOrderClick(order)"
:key="order && order.id ? order.id : `order-${index}`"
:class="{ 'pending-payment': order && order.status === 'pending' }"
@click="order ? handleOrderClick(order) : null"
v-if="order"
>
<view class="order-header">
<view class="order-info">
@ -31,8 +32,8 @@
{{ formatTime(order.create_time) }}
</view>
</view>
<view :class="['order-status',getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
<view :class="['order-status',getStatusClass(order && order.status)]">
{{ getStatusText(order && order.status) }}
</view>
</view>
@ -77,15 +78,10 @@
<text class="detail-label">课时数量</text>
<text class="detail-value">{{ order.course_count }}</text>
</view>
<view class="detail-row" v-if="order.valid_period">
<text class="detail-label">有效期</text>
<text class="detail-value">{{ order.valid_period }}</text>
</view>
</view>
<!-- 备注信息 -->
<view class="order-remark" v-if="order.remark">
<view class="order-remark">
<view class="remark-label">备注</view>
<view class="remark-content">{{ order.remark }}</view>
</view>
@ -129,6 +125,13 @@ export default {
//
handleOrderClick(order) {
console.log('点击订单:', order)
// order
if (!order) {
console.warn('订单数据不存在,无法处理点击事件')
return
}
if (order.status === 'pending') {
//
this.$emit('pay-order', order)
@ -140,6 +143,8 @@ export default {
//
getStatusClass(status) {
if (!status) return 'status-default'
const statusMap = {
'pending': 'status-pending',
'paid': 'status-paid',
@ -153,6 +158,8 @@ export default {
//
getStatusText(status) {
if (!status) return '未知状态'
const statusMap = {
'pending': '待支付',
'paid': '已支付',
@ -206,7 +213,6 @@ export default {
background: rgba(255, 193, 7, 0.05);
&::after {
content: '点击支付';
position: absolute;
bottom: 16rpx;
right: 16rpx;

179
uniapp/components/service-list-card/index.vue

@ -1,11 +1,24 @@
<!--服务列表内容组件-->
<template>
<view class="service-list-card">
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-icon">🔄</view>
<view class="loading-text">加载中...</view>
</view>
<!-- 错误状态 -->
<view class="error-state" v-else-if="error">
<view class="error-icon"></view>
<view class="error-text">{{ error }}</view>
<view class="retry-button" @click="fetchServiceList">重试</view>
</view>
<!-- 服务列表 -->
<view class="service-list" v-if="serviceList && serviceList.length > 0">
<view class="service-list" v-else-if="actualServiceList && actualServiceList.length > 0">
<view
class="service-item"
v-for="(service, index) in serviceList"
v-for="(service, index) in actualServiceList"
:key="service.id || index"
@click="viewServiceDetail(service)"
>
@ -65,15 +78,10 @@
</view>
<view class="log-detail-item" v-if="log.duration">
<text class="detail-label">服务时</text>
<text class="detail-value">{{ log.duration }}分钟</text>
<text class="detail-label">服务时</text>
<text class="detail-value">{{ log.updated_at }}</text>
</view>
<view class="log-detail-item" v-if="log.service_location">
<text class="detail-label">服务地点</text>
<text class="detail-value">{{ log.service_location }}</text>
</view>
<view class="log-detail-item" v-if="log.customer_feedback">
<text class="detail-label">客户反馈</text>
<text class="detail-value feedback">{{ log.customer_feedback }}</text>
@ -111,10 +119,6 @@
<text class="stat-label">已完成</text>
<text class="stat-value">{{ service.completed_count || 0 }}</text>
</view>
<view class="stat-item" v-if="service.total_count">
<text class="stat-label">剩余</text>
<text class="stat-value highlight">{{ (service.total_count - (service.completed_count || 0)) }}</text>
</view>
</view>
</view>
</view>
@ -129,6 +133,8 @@
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
name: 'ServiceListCard',
props: {
@ -136,10 +142,92 @@ export default {
serviceList: {
type: Array,
default: () => []
},
// ID
studentId: {
type: [String, Number],
default: null
}
},
data() {
return {
internalServiceList: [],
loading: false,
error: null
}
},
computed: {
// 使
actualServiceList() {
// serviceList使
if (this.serviceList && this.serviceList.length > 0) {
return this.serviceList
}
// 使
return this.internalServiceList
}
},
mounted() {
// serviceListstudentId
if ((!this.serviceList || this.serviceList.length === 0) && this.studentId) {
this.fetchServiceList()
}
},
watch: {
studentId: {
handler(newVal) {
if (newVal && (!this.serviceList || this.serviceList.length === 0)) {
this.fetchServiceList()
}
},
immediate: true
}
},
methods: {
//
async fetchServiceList() {
if (!this.studentId) {
console.error('获取服务记录失败:学员ID不能为空')
return
}
this.loading = true
this.error = null
try {
const response = await apiRoute.getStudentServiceList({
student_id: this.studentId
})
if (response.code === 1) {
this.internalServiceList = response.data || []
} else {
this.error = response.msg || '获取服务记录失败'
console.error('获取服务记录失败:', response.msg)
uni.showToast({
title: this.error,
icon: 'none',
duration: 2000
})
}
} catch (error) {
this.error = '网络请求失败'
console.error('获取服务记录异常:', error)
uni.showToast({
title: '网络请求失败',
icon: 'none',
duration: 2000
})
} finally {
this.loading = false
}
},
//
viewServiceDetail(service) {
this.$emit('view-detail', service)
@ -206,6 +294,69 @@ export default {
padding: 0;
}
//
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
text-align: center;
}
.loading-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: #999999;
}
//
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
text-align: center;
}
.error-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.error-text {
font-size: 28rpx;
color: #F44336;
margin-bottom: 32rpx;
line-height: 1.4;
}
.retry-button {
padding: 12rpx 32rpx;
background: #29D3B4;
color: #ffffff;
font-size: 26rpx;
border-radius: 24rpx;
transition: all 0.3s ease;
&:active {
background: #24B89E;
transform: scale(0.98);
}
}
.service-list {
display: flex;
flex-direction: column;

285
uniapp/cross-platform-compatibility-report.md

@ -1,285 +0,0 @@
# UniApp 学员端跨平台兼容性报告
## 概述
本报告详细说明UniApp学员端在不同平台的兼容性验证结果,确保在H5、小程序、APP等平台上都能正常运行。
## 平台支持
- ✅ **H5端**: 浏览器环境,支持桌面和移动端
- ✅ **微信小程序**: 微信生态内运行
- ✅ **APP端**: iOS/Android原生应用
- ✅ **其他小程序**: 支付宝、百度、字节跳动等
## 响应式设计验证
### 1. 布局兼容性 ✅
#### 1.1 CSS Grid vs Flexbox选择
**修改前 (CSS Grid)**:
```css
.feature-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25rpx;
}
```
**修改后 (Flexbox - 跨平台兼容)**:
```css
.feature-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
padding: 0 10rpx;
}
.feature-item {
width: 30%;
margin-bottom: 25rpx;
box-sizing: border-box;
}
```
**兼容性说明**:
- ❌ CSS Grid 在部分小程序端支持不完善
- ✅ Flexbox 在所有UniApp支持平台都有良好兼容性
#### 1.2 条件编译支持
```css
/* 小程序端兼容 */
// #ifdef MP-WEIXIN
min-height: 140rpx;
// #endif
/* H5端兼容 */
// #ifdef H5
cursor: pointer;
transition: all 0.3s ease;
// #endif
```
### 2. 屏幕尺寸适配 ✅
#### 2.1 rpx单位使用
- ✅ 使用rpx单位确保不同设备尺寸自适应
- ✅ 字体大小: 26rpx-48rpx 范围
- ✅ 间距和内边距: 10rpx-30rpx 范围
- ✅ 图标尺寸: 60rpx x 60rpx
#### 2.2 屏幕适配测试
| 设备类型 | 屏幕尺寸 | 布局表现 | 状态 |
|---------|---------|---------|------|
| iPhone 12 | 390x844 | 正常显示 | ✅ |
| iPhone SE | 375x667 | 正常显示 | ✅ |
| Android 中等 | 360x640 | 正常显示 | ✅ |
| iPad | 768x1024 | 正常显示 | ✅ |
| 桌面端 | 1920x1080 | 正常显示 | ✅ |
### 3. 交互兼容性 ✅
#### 3.1 触摸事件处理
```css
.feature-item:active {
transform: scale(0.95);
background: rgba(41, 211, 180, 0.2);
}
```
- ✅ 支持触摸反馈效果
- ✅ H5端支持鼠标点击
- ✅ 小程序端支持触摸交互
- ✅ APP端支持原生触摸
#### 3.2 导航兼容性
```javascript
// UniApp 统一导航API
this.$navigateTo({
url: '/pages/student/timetable/index'
})
```
- ✅ 所有平台使用统一的导航API
- ✅ 路由参数传递正常
- ✅ 页面栈管理正确
### 4. 图片资源兼容性 ✅
#### 4.1 图片路径统一
```javascript
:src="$util.img('/uniapp_src/static/images/index/timetable.png')"
```
- ✅ 使用统一图片工具函数
- ✅ 支持不同平台的图片路径解析
- ✅ 自动适配CDN或本地路径
#### 4.2 图片格式支持
- ✅ PNG格式: 所有平台支持
- ✅ JPG格式: 所有平台支持
- ✅ WebP格式: H5端支持,小程序部分支持
- ✅ SVG格式: H5端支持,其他平台需转换
## Mock数据跨平台兼容性 ✅
### 1. 网络请求兼容
```javascript
// 统一的网络请求处理
uni.request({
url: Api_url + options.url,
method: options.method || 'GET',
data: options.data,
success: (res) => { /* 处理响应 */ }
})
```
### 2. 本地存储兼容
```javascript
// 统一的存储API
uni.getStorageSync("token")
uni.setStorageSync("token", value)
```
### 3. 环境变量支持
```javascript
// 跨平台环境变量读取
const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true'
```
## 性能优化 ✅
### 1. 渲染性能
- ✅ 使用v-if/v-show合理控制DOM渲染
- ✅ 列表渲染使用v-for with key
- ✅ 图片懒加载在长列表中应用
### 2. 内存管理
- ✅ 页面卸载时清理定时器
- ✅ 合理使用组件生命周期
- ✅ 避免内存泄漏
### 3. 包体大小
- ✅ 图片资源压缩
- ✅ 代码压缩和混淆
- ✅ 按需加载组件
## 平台特性验证
### 1. H5端特性 ✅
- ✅ 浏览器兼容性: Chrome, Safari, Firefox
- ✅ 移动端浏览器适配
- ✅ PWA特性支持 (可选)
- ✅ SEO优化支持
### 2. 小程序端特性 ✅
- ✅ 微信小程序审核规范遵循
- ✅ 小程序生命周期正确处理
- ✅ 分包加载策略
- ✅ 小程序性能优化
### 3. APP端特性 ✅
- ✅ 原生导航栏集成
- ✅ 状态栏适配
- ✅ 原生API调用
- ✅ 热更新支持
## 测试结果汇总
### 功能测试覆盖率
| 功能模块 | H5端 | 小程序端 | APP端 | 覆盖率 |
|---------|------|---------|-------|--------|
| 学员登录 | ✅ | ✅ | ✅ | 100% |
| 首页展示 | ✅ | ✅ | ✅ | 100% |
| 个人中心 | ✅ | ✅ | ✅ | 100% |
| 课程表 | ✅ | ✅ | ✅ | 100% |
| 作业管理 | ✅ | ✅ | ✅ | 100% |
| Mock数据 | ✅ | ✅ | ✅ | 100% |
### 性能测试结果
| 性能指标 | H5端 | 小程序端 | APP端 | 目标值 |
|---------|------|---------|-------|--------|
| 首屏加载时间 | 1.2s | 0.8s | 0.6s | <2s |
| 页面切换时间 | 0.3s | 0.2s | 0.1s | <0.5s |
| 内存使用 | 45MB | 35MB | 55MB | <100MB |
| 包体大小 | - | 2.1MB | 8.5MB | <10MB |
## 兼容性问题及解决方案
### 1. 已解决的问题 ✅
#### 问题1: CSS Grid 兼容性
**问题**: CSS Grid在部分小程序端不支持
**解决方案**: 改用Flexbox布局
```css
/* 替换前 */
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 替换后 */
display: flex;
flex-wrap: wrap;
justify-content: space-around;
```
#### 问题2: 过渡动画兼容性
**问题**: CSS transition在部分平台性能差
**解决方案**: 使用条件编译,仅在支持的平台启用
```css
// #ifdef H5
transition: all 0.3s ease;
// #endif
```
### 2. 注意事项
#### 2.1 小程序端限制
- 不支持部分CSS3属性
- 网络请求需要域名白名单
- 本地存储有大小限制
#### 2.2 APP端注意事项
- 需要处理不同状态栏高度
- 原生API调用需要权限处理
- 不同Android版本兼容性
## 部署验证清单
### 1. 开发环境验证
```bash
# H5端测试
npm run dev:h5
# 小程序端测试
npm run dev:mp-weixin
# APP端测试
npm run dev:app
```
### 2. 生产环境验证
```bash
# 构建验证
npm run build:h5
npm run build:mp-weixin
npm run build:app
# 性能测试
npm run test:performance
# 兼容性测试
npm run test:compatibility
```
## 结论
✅ **跨平台兼容性验证通过**
**主要成果**:
1. **布局兼容性**: 使用Flexbox替代CSS Grid,确保所有平台布局一致
2. **响应式设计**: 采用rpx单位和条件编译,适配不同屏幕尺寸
3. **交互兼容性**: 统一的触摸反馈和导航API
4. **性能优化**: 合理的资源管理和渲染优化
5. **Mock数据**: 在所有平台都能正常工作
**技术特点**:
- 100% 使用UniApp原生API
- 0 第三方兼容性问题
- 全平台统一的用户体验
- 高性能的渲染表现
系统已通过H5、小程序、APP三端的全面兼容性测试,可以安全部署到生产环境。

311
uniapp/pages/market/clue/class_arrangement_detail.vue

@ -1,17 +1,15 @@
<template>
<view class="course-schedule">
<!-- Header -->
<view class="header">
<view class="back-btn" @tap="goBack">
<text class="back-icon"></text>
</view>
<text class="title">课程安排详情</text>
</view>
<!-- Course Info -->
<view class="course-info">
<text class="course-title">课程安排详情</text>
<text class="course-time">日期{{ course_info.course_date }} {{ course_info.time_slot }}</text>
<view class="course-detail">
<text class="course-item">日期{{ schedule_info.course_date }}</text>
<text class="course-item">课程名称{{ schedule_info.course_name }}</text>
<text class="course-item">课程时间{{ schedule_info.time_slot }}</text>
<text class="course-item">主教练{{ schedule_info.coach_name || '待安排' }}</text>
<text class="course-item">场地信息{{ schedule_info.venue_name }}</text>
</view>
</view>
<!-- Formal Students Section -->
@ -30,10 +28,9 @@
<view class="student-info">
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄{{ stu.age || '未知' }}</view>
<view class="course-status">课程状态{{ getCourseTypeText(stu.course_type) }}</view>
<view class="course-arrangement">课程安排{{ getScheduleTypeText(stu.schedule_type) }}</view>
<view class="remaining-hours">剩余课时{{ stu.remaining_hours || 0 }}</view>
<view class="expiry-date">到期时间{{ stu.expiry_date || '未设置' }}</view>
<view class="course-status">课程状态{{ stu.courseStatus }}</view>
<view class="course-progress">上课情况{{ stu.course_progress.used }}/{{ stu.course_progress.total }} ({{ stu.course_progress.percentage }}%)</view>
<view class="expiry-date" v-if="stu.student_course_info">到期时间{{ stu.student_course_info.end_date || '未设置' }}</view>
</view>
</view>
@ -72,10 +69,9 @@
<view class="student-info">
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄{{ stu.age || '未知' }}</view>
<view class="course-status">课程状态{{ getCourseTypeText(stu.course_type) }}</view>
<view class="course-arrangement">课程安排{{ getScheduleTypeText(stu.schedule_type) }}</view>
<view class="remaining-hours">剩余课时{{ stu.remaining_hours || 0 }}</view>
<view class="expiry-date">到期时间{{ stu.expiry_date || '未设置' }}</view>
<view class="course-status">课程状态{{ stu.courseStatus }}</view>
<view class="course-progress">上课情况{{ stu.course_progress.used }}/{{ stu.course_progress.total }} ({{ stu.course_progress.percentage }}%)</view>
<view class="expiry-date">到期时间{{ stu.expiryDate || '未设置' }}</view>
</view>
</view>
@ -110,24 +106,10 @@
<!-- Customer Selection - 只在没有预设学生时显示 -->
<view v-if="!presetStudent" class="form-section">
<text class="form-label">客户选择</text>
<view class="search-tabs">
<view
:class="['tab-btn', { active: searchType === 'phone' }]"
@tap="searchType = 'phone'"
>
手机号检索
</view>
<view
:class="['tab-btn', { active: searchType === 'name' }]"
@tap="searchType = 'name'"
>
姓名检索
</view>
</view>
<input
v-model="searchQuery"
:placeholder="searchType === 'phone' ? '请输入手机号' : '请输入姓名'"
placeholder="请输入手机号或姓名"
class="search-input"
@input="searchStudents"
/>
@ -140,9 +122,12 @@
:class="['student-item', { selected: selectedStudent && selectedStudent.id === student.id }]"
@tap="selectStudent(student)"
>
<view class="student-avatar">{{ student.name.charAt(0) }}</view>
<view class="student-avatar">{{ student.name ? student.name.charAt(0) : '?' }}</view>
<view class="student-details">
<view class="student-name">{{ student.name }}</view>
<view class="student-name">
{{ student.name }}
<text v-if="student.is_formal_student" class="formal-badge">正式学员</text>
</view>
<view class="student-phone">{{ student.phone }}</view>
</view>
<view v-if="selectedStudent && selectedStudent.id === student.id" class="check-icon">
@ -156,10 +141,10 @@
<view v-if="presetStudent" class="form-section">
<text class="form-label">选中学员</text>
<view class="preset-student">
<view class="student-avatar">{{ presetStudent.name.charAt(0) }}</view>
<view class="student-avatar">{{ presetStudent.name ? presetStudent.name.charAt(0) : '?' }}</view>
<view class="student-details">
<view class="student-name">{{ presetStudent.name }}</view>
<view class="student-phone">{{ presetStudent.phone }}</view>
<view class="student-name">{{ presetStudent.name || '未知学员' }}</view>
<view class="student-phone">{{ presetStudent.phone || '未知手机号' }}</view>
</view>
</view>
</view>
@ -212,7 +197,7 @@
<view class="leave-label">请假原因</view>
<view class="leave-input">
<fui-textarea v-model="leaveReason" placeholder="请输入请假原因" :isCounter="true" :maxlength="200"
:minHeight="200" :isAutoHeight="true"></fui-textarea>
minHeight="200" :isAutoHeight="true"></fui-textarea>
</view>
<view class="leave-buttons">
<fui-button background="#434544" color="#fff" borderColor="#666" btnSize="medium"
@ -231,7 +216,7 @@
data() {
return {
course_id: '',
course_info: [],
schedule_info: {}, //
date: '',
students: [], //
formalStudents: [], //
@ -245,27 +230,19 @@
//
showModal: false,
searchType: 'phone',
searchQuery: '',
courseArrangement: '1', // 1=, 2=
remarks: '',
selectedStudent: null,
currentSlot: null,
searchResults: [],
presetStudent: null, //
// Mock student data for search
allStudents: [
{ id: 1, name: '张小明', phone: '13800138001', age: 8 },
{ id: 2, name: '李小红', phone: '13800138002', age: 9 },
{ id: 3, name: '王小华', phone: '13800138003', age: 7 },
{ id: 4, name: '赵小强', phone: '13800138004', age: 10 }
]
presetStudent: null //
};
},
onLoad(query) {
console.log('onLoad 参数:', query);
this.course_id = query.id || '';
// id schedule_id
this.course_id = query.id || query.schedule_id || '';
this.resource_id = query.resource_id || '';
this.student_id = query.student_id || '';
@ -276,7 +253,16 @@
this.loadPresetStudent();
}
this.courseInfo();
// IDAPI
if (this.course_id) {
this.getScheduleDetail();
} else {
console.error('缺少课程ID参数');
uni.showToast({
title: '缺少课程ID参数',
icon: 'none'
});
}
},
methods: {
//
@ -287,21 +273,25 @@
//
async loadPresetStudent() {
try {
// API
// 使mock
const mockStudent = this.allStudents.find(s => s.id == this.student_id);
if (mockStudent) {
this.presetStudent = mockStudent;
this.selectedStudent = mockStudent;
// API
const res = await apiRoute.getCustomerResourcesInfo({ resource_sharing_id: this.resource_id });
if (res.code === 1 && res.data) {
//
const isFormalStudent = await this.checkIsFormalStudent(res.data.member_id);
this.presetStudent = {
id: res.data.id,
name: res.data.name,
phone: res.data.phone_number,
age: res.data.age,
member_id: res.data.member_id,
resource_id: res.data.id,
is_formal_student: isFormalStudent
};
this.selectedStudent = this.presetStudent;
console.log('加载预设学生信息:', this.presetStudent);
}
// TODO: API
// const res = await apiRoute.getStudentInfo({ student_id: this.student_id });
// if (res.code === 1) {
// this.presetStudent = res.data;
// this.selectedStudent = res.data;
// }
} catch (error) {
console.error('加载预设学生信息失败:', error);
}
@ -340,20 +330,89 @@
this.remarks = '';
},
//
searchStudents() {
// -
async searchStudents() {
if (!this.searchQuery.trim()) {
this.searchResults = [];
return;
}
this.searchResults = this.allStudents.filter(student => {
if (this.searchType === 'phone') {
return student.phone.includes(this.searchQuery);
//
const isPhoneNumber = /^\d+$/.test(this.searchQuery.trim());
try {
uni.showLoading({
title: '搜索中...'
});
//
const params = {};
if (isPhoneNumber) {
params.phone_number = this.searchQuery.trim();
} else {
return student.name.includes(this.searchQuery);
params.name = this.searchQuery.trim();
}
});
const res = await apiRoute.getCustomerResourcesAll(params);
uni.hideLoading();
if (res.code === 1 && Array.isArray(res.data)) {
//
this.searchResults = await Promise.all(res.data.map(async (student) => {
//
const isFormalStudent = await this.checkIsFormalStudent(student.member_id);
return {
id: student.id,
name: student.name,
phone: student.phone_number,
age: student.age,
member_id: student.member_id,
resource_id: student.id,
is_formal_student: isFormalStudent
};
}));
} else {
this.searchResults = [];
}
} catch (error) {
uni.hideLoading();
console.error('搜索学员失败:', error);
this.searchResults = [];
}
},
//
async checkIsFormalStudent(memberId) {
if (!memberId) {
return false;
}
try {
// API
const res = await apiRoute.getStudentCourseInfo({ member_id: memberId });
if (res.code === 1 && res.data && Array.isArray(res.data)) {
//
const currentTime = new Date().getTime();
return res.data.some(course => {
//
if (course.expiry_date) {
const expiryTime = new Date(course.expiry_date).getTime();
return expiryTime >= currentTime && course.remaining_hours > 0;
}
//
return course.remaining_hours > 0;
});
}
return false;
} catch (error) {
console.error('检查正式学员状态失败:', error);
return false;
}
},
//
@ -371,16 +430,27 @@
return;
}
//
if (this.courseArrangement === '2') {
if (!this.selectedStudent.is_formal_student) {
uni.showToast({
title: '只有正式学员才能选择固定课',
icon: 'none'
});
return;
}
}
//
const data = {
'resources_id': this.selectedStudent.resource_id || this.resource_id,
'person_type': 'customer_resource',
'schedule_id': this.course_id,
'course_date': this.course_info.course_date,
'time_slot': this.course_info.time_slot,
'course_date': this.schedule_info.course_date,
'time_slot': this.schedule_info.time_slot,
'position': this.currentSlot.index,
'schedule_type': this.currentSlot.type === 'waiting' ? 2 : 1,
'course_type': parseInt(this.courseArrangement),
'course_type': this.currentSlot.type === 'waiting' ? 3 : parseInt(this.courseArrangement),
'remark': this.remarks
};
@ -401,7 +471,7 @@
this.closeModal();
//
await this.courseInfo();
await this.getScheduleDetail();
} else {
uni.showToast({
title: res.msg || '添加失败',
@ -427,7 +497,7 @@
return typeMap[type] || '未知';
},
viewStudent(stu) {
console.log(stu, this.course_info);
console.log(stu, this.schedule_info);
//
uni.showActionSheet({
@ -451,7 +521,7 @@
const params = {
resources_id: stu.resources_id,
id: this.course_info.id
id: this.schedule_info.id
};
const result = await apiRoute.schedule_del(params);
@ -465,7 +535,7 @@
});
//
await this.courseInfo();
await this.getScheduleDetail();
} else {
uni.showToast({
title: result.msg || '取消课程失败',
@ -570,52 +640,57 @@
return [];
}
},
async courseInfo() {
async getScheduleDetail() {
try {
console.log('开始获取课程信息, id:', this.course_id);
let res = await apiRoute.courseInfo({
'id': this.course_id
console.log('开始获取课程安排详情, schedule_id:', this.course_id);
let res = await apiRoute.getScheduleDetail({
'schedule_id': this.course_id
})
console.log('课程信息响应:', res);
console.log('课程安排详情响应:', res);
if (res.code === 1 && res.data) {
this.course_info = res.data;
//
await this.scheduleList();
this.schedule_info = res.data.schedule_info;
this.formalStudents = res.data.formal_students || [];
this.waitingStudents = res.data.waiting_students || [];
//
this.updateAvailableCapacity();
} else {
uni.showToast({
title: res.msg || '获取课程信息失败',
title: res.msg || '获取课程安排详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取课程信息失败:', error);
console.error('获取课程安排详情失败:', error);
uni.showToast({
title: '获取课程信息失败',
title: '获取课程安排详情失败',
icon: 'none'
});
}
},
//
updateAvailableCapacity() {
// course_info
if (!this.course_info) return;
// schedule_info
if (!this.schedule_info) return;
console.log('更新可用容量 - 课程容量:', this.course_info.available_capacity, '正式学员数量:', this.formalStudents.length);
console.log('更新可用容量 - 课程最大容量:', this.schedule_info.max_students, '正式学员数量:', this.formalStudents.length);
//
const occupiedFormalSeats = this.formalStudents.length;
//
const totalCapacity = parseInt(this.course_info.available_capacity) || 0;
//
const remainingFormalSeats = Math.max(0, totalCapacity - occupiedFormalSeats);
//
const maxStudents = parseInt(this.schedule_info.max_students) || 0;
let remainingFormalSeats = 0;
if (maxStudents === 0) {
// max_students0
// 6
remainingFormalSeats = Math.max(1, 6 - occupiedFormalSeats);
} else {
//
remainingFormalSeats = Math.max(0, maxStudents - occupiedFormalSeats);
}
console.log('计算后的正式位剩余容量:', remainingFormalSeats);
@ -625,7 +700,7 @@
this.formalEmptySeats.push(i);
}
//
// 2
const remainingWaitingSeats = Math.max(0, 2 - this.waitingStudents.length);
this.waitingEmptySeats = [];
for (let i = 1; i <= remainingWaitingSeats; i++) {
@ -641,11 +716,11 @@
'resources_id': this.resource_id,
'person_type': 'customer_resource',
'schedule_id': this.course_id,
'course_date': this.course_info.course_date,
'time_slot': this.course_info.time_slot,
'course_date': this.schedule_info.course_date,
'time_slot': this.schedule_info.time_slot,
'position': index, //
'schedule_type': type === 'waiting' ? 2 : 1, // 1=, 2=
'course_type': type === 'waiting' ? 2 : 1 // 1=, 2=
'course_type': type === 'waiting' ? 3 : 1 // 3=, 1=
};
try {
@ -664,7 +739,7 @@
});
//
await this.courseInfo();
await this.getScheduleDetail();
} else {
uni.showToast({
title: res.msg || '添加失败',
@ -699,7 +774,7 @@
//
const params = {
resources_id: this.currentStudent.resources_id,
id: this.course_info.id,
id: this.schedule_info.id,
remark: this.leaveReason
};
@ -716,7 +791,7 @@
this.leaveReason = ''; //
//
this.courseInfo().then(() => {
this.getScheduleDetail().then(() => {
//
this.updateAvailableCapacity();
});
@ -779,7 +854,6 @@
.course-info {
font-size: 28rpx;
margin: 30rpx;
background: #434544;
border-radius: 16rpx;
padding: 30rpx 20rpx;
@ -789,11 +863,16 @@
font-size: 32rpx;
font-weight: bold;
margin-bottom: 16rpx;
display: block;
}
.course-time {
color: #29d3b4;
font-size: 26rpx;
.course-detail {
.course-item {
color: #29d3b4;
font-size: 26rpx;
margin-bottom: 8rpx;
display: block;
}
}
}
@ -1070,6 +1149,18 @@
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
display: flex;
align-items: center;
gap: 16rpx;
.formal-badge {
background: #29d3b4;
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
white-space: nowrap;
}
}
.student-phone {

68
uniapp/pages/market/clue/clue_info.vue

@ -553,7 +553,7 @@ export default {
switch (action.key) {
case 'course_arrangement':
this.$navigateToPage(`/pages/market/clue/class_arrangement`, {
resource_id: this.clientInfo.resource_id,
resource_id: this.clientInfo.id,
student_id: student.id
})
break
@ -803,6 +803,7 @@ export default {
const res = await apiRoute.xs_addStudent(result.studentData)
if (res.code === 1) {
this.$refs.studentEditPopup.close()
uni.showToast({ title: '保存成功', icon: 'success' })
//
await this.getStudentList()
@ -919,29 +920,35 @@ export default {
async getServiceList(studentId = null) {
if (!this.clientInfo?.resource_id) return
const targetStudentId = studentId || this.currentStudent?.id
// 使
this.serviceList = [
{
id: 1,
student_id: targetStudentId || 1,
service_name: '体能评估服务',
description: '专业的体能测试和评估',
status: 'completed',
total_count: 1,
completed_count: 1,
logs: [
{
id: 1,
service_time: '2024-01-15 14:00:00',
service_content: '完成全面体能测试',
service_staff: '王教练',
duration: 60,
service_rating: 5,
status: 'completed'
}
]
if (!targetStudentId) {
uni.showToast({ title: '请先选择学生', icon: 'none' })
return
}
try {
const response = await apiRoute.getStudentServiceList({
student_id: targetStudentId
})
if (response.code === 1) {
this.serviceList = response.data || []
} else {
console.error('获取服务记录失败:', response.msg)
uni.showToast({
title: response.msg || '获取服务记录失败',
icon: 'none'
})
this.serviceList = []
}
]
} catch (error) {
console.error('获取服务记录异常:', error)
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
this.serviceList = []
}
},
//
@ -994,7 +1001,7 @@ export default {
//
async processPayment(order) {
const paymentType = order._raw?.payment_type || order.payment_type
console.log('paymentType', paymentType)
try {
switch(paymentType) {
case 'cash':
@ -1057,18 +1064,7 @@ export default {
//
showQRCodePayment(order) {
uni.showModal({
title: '扫码支付',
content: `请使用支付宝或微信扫码支付 ¥${order.total_amount}`,
confirmText: '已完成支付',
cancelText: '取消支付',
success: async (res) => {
if (res.confirm) {
//
await this.updateOrderStatus(order, 'paid', `QR${Date.now()}`)
}
}
})
console.log('扫码支付:', order)
},
//

234
uniapp/student-functionality-validation.md

@ -1,234 +0,0 @@
# 学员端功能验证清单
## 概述
根据PRP要求,本文档详细说明学员端页面功能和Mock数据集成的验证清单。
## 验证环境
- **Mock数据状态**: 默认开启 (VUE_APP_MOCK_ENABLED=true)
- **测试平台**: UniApp H5/小程序/APP
- **验证时间**: 2024年1月
## 核心功能验证
### 1. 学员端首页功能 ✅
**页面路径**: `pages/student/index/index.vue`
#### 1.1 用户信息展示
- [x] 学员头像显示
- [x] 学员姓名显示
- [x] Mock数据自动加载
#### 1.2 体测数据模块
- [x] 综合评分显示 (Mock数据: 85分)
- [x] 身高体重数据 (Mock数据: 165cm, 55kg)
- [x] 测评时间显示
- [x] "更多"按钮跳转功能
#### 1.3 课程预告模块
- [x] 最近课程信息显示
- [x] 课程时间和地点显示
- [x] "详情"按钮跳转功能
#### 1.4 课后作业模块
- [x] 待提交作业列表
- [x] 已提交作业列表
- [x] 作业上传功能
- [x] 作业详情查看
#### 1.5 功能快捷入口 ✅ (新增)
- [x] 课程表快捷入口
- [x] 作业管理快捷入口
- [x] 学习资料快捷入口
- [x] 消息中心快捷入口
- [x] 订单管理快捷入口
- [x] 个人中心快捷入口
### 2. 个人中心功能 ✅
**页面路径**: `pages/student/my/my.vue`
#### 2.1 用户信息区域
- [x] 用户头像和姓名显示
- [x] 设置按钮功能
- [x] 个人资料跳转
#### 2.2 统计信息
- [x] 我的课程数量统计
- [x] 已上课时统计
- [x] 待上课时统计
#### 2.3 功能菜单 ✅ (增强)
- [x] 课时消耗页面跳转
- [x] 我的订单页面跳转
- [x] 我的课表页面跳转 (新增)
- [x] 我的教练页面跳转
- [x] 作业管理页面跳转 (新增)
- [x] 意见反馈页面跳转
- [x] 我的消息页面跳转
- [x] 右箭头图标显示 (新增)
### 3. Mock数据集成验证 ✅
#### 3.1 API响应格式验证
```json
{
"code": 1,
"message": "success",
"data": { ... },
"timestamp": 1640995200000
}
```
#### 3.2 学员信息Mock数据
- [x] `/customerResourcesAuth/info` 接口Mock
- [x] 学员基本信息: 李小明, 15岁
- [x] 课程统计数据: 8门课程, 15课时已上, 5课时待上
- [x] 头像和联系方式
#### 3.3 体测数据Mock
- [x] `/xy/physicalTest` 接口Mock
- [x] 身高体重数据: 165cm, 55kg
- [x] 综合评分: 85分
- [x] 测试时间: 2024-01-10
#### 3.4 课程安排Mock数据
- [x] `/xy/personCourseSchedule` 接口Mock
- [x] 课程列表数据 (篮球训练、足球基础)
- [x] 时间安排和状态管理
- [x] 场地和教练信息
#### 3.5 作业数据Mock
- [x] `/xy/assignment` 接口Mock
- [x] 待提交作业列表
- [x] 已提交作业列表
- [x] 作业状态过滤 (status参数)
#### 3.6 教练信息Mock
- [x] `/xy/personCourseSchedule/getMyCoach` 接口Mock
- [x] 教练详细信息
- [x] 专业领域和经验
### 4. 跨平台兼容性验证
#### 4.1 响应式设计
- [x] H5端显示正常
- [x] 小程序端兼容性
- [x] APP端兼容性
- [x] 不同屏幕尺寸适配
#### 4.2 交互体验
- [x] 触摸反馈效果
- [x] 页面切换动画
- [x] 加载状态显示
- [x] 错误提示处理
#### 4.3 性能表现
- [x] Mock数据加载速度
- [x] 页面渲染性能
- [x] 内存使用合理性
### 5. 环境变量控制验证
#### 5.1 开发环境 (.env.development)
- [x] VUE_APP_MOCK_ENABLED=true 生效
- [x] Mock数据优先加载
- [x] API失败时自动切换Mock
#### 5.2 生产环境 (.env.production)
- [x] VUE_APP_MOCK_ENABLED=false 设置
- [x] 真实API请求正常
- [x] Mock功能完全关闭
## 验证结果
### ✅ 通过的验证项目
1. **学员端首页功能完整** - 包含体测、课程、作业、快捷入口
2. **个人中心功能增强** - 添加课表、作业管理入口和导航图标
3. **Mock数据集成成功** - 支持8个主要API的Mock数据
4. **环境变量控制正常** - 开发/生产环境切换正确
5. **跨平台兼容性良好** - H5/小程序/APP均可正常运行
6. **响应式设计合理** - 各种屏幕尺寸适配正确
### 📋 功能清单确认
- [x] 学员信息展示和管理
- [x] 课程列表和课表查看
- [x] 学习资料入口 (待开发提示)
- [x] 作业管理 (上传、查看、提交)
- [x] 消息中心跳转
- [x] 订单管理跳转
- [x] 个人中心功能完善
- [x] Mock数据环境变量控制 (默认开启)
- [x] 正常渲染和功能交互
## 测试用例
### 测试用例1: Mock数据开关控制
```bash
# 开发环境测试
# 修改 .env.development: VUE_APP_MOCK_ENABLED=true
# 预期结果: 直接返回Mock数据,显示"使用模拟数据"提示
# 生产环境测试
# 修改 .env.production: VUE_APP_MOCK_ENABLED=false
# 预期结果: 发送真实API请求
```
### 测试用例2: 学员端页面导航
```bash
# 首页功能测试
1. 打开学员端首页 → 显示体测数据、课程预告、作业列表
2. 点击快捷入口 → 跳转到对应功能页面
3. 查看课程预告 → 显示最近课程信息
4. 作业上传测试 → 选择文件并提交
# 个人中心测试
1. 打开个人中心 → 显示统计信息和功能菜单
2. 点击功能菜单 → 跳转到对应页面
3. 查看个人信息 → 正确显示学员数据
```
### 测试用例3: Mock数据响应
```javascript
// API请求测试
const response = await apiRoute.xy_memberInfo({})
// 预期响应格式
{
"code": 1,
"message": "success",
"data": {
"id": 1001,
"name": "李小明",
"classes_count": 8,
// ... 其他字段
}
}
```
## 部署验证
### 1. 本地开发验证
```bash
# 在uniapp目录下运行
npm install
npm run dev:h5
# 访问学员端首页和个人中心页面
```
### 2. Mock数据验证
```bash
# 检查Mock服务状态
# 开发者工具控制台查看: "Mock服务状态: 已启用"
# 网络请求显示: "使用模拟数据" 提示
```
## 结论
✅ **学员端功能实现验证通过**
所有核心功能已实现并通过验证:
- **PRP要求完全满足**: 环境变量控制Mock数据(默认开启),正常渲染和功能交互
- **学员端页面功能完整**: 首页包含所有必要的操作按钮和功能模块
- **个人中心功能完善**: 包含用户信息、订单管理、消息中心、作业管理等
- **Mock数据策略成功**: 支持8个主要API的完整Mock数据
- **跨平台兼容性良好**: H5、小程序、APP均可正常运行
系统已准备好用于生产环境部署和进一步的功能扩展。

280
uniapp权限管理配置文档.md

@ -1,280 +0,0 @@
# UniApp 权限管理配置文档
## 📋 概述
本文档详细列出了UniApp教育培训管理系统中所有页面的权限配置建议,用于实现精细化的菜单权限管理。系统支持**员工端**(市场、教练、销售)和**会员端**(学员、家长)两种登录方式。
## 🔐 权限等级说明
| 权限等级 | 说明 | 适用场景 |
|---------|------|---------|
| **公开访问** | 无需登录即可访问 | 登录页、隐私协议等 |
| **登录验证** | 需要有效Token | 大部分功能页面 |
| **角色验证** | 需要特定角色权限 | 业务功能页面 |
| **管理员权限** | 需要管理员级别权限 | 人员管理、系统配置 |
| **开发权限** | 仅开发环境可访问 | 测试页面、Demo页面 |
## 👥 用户角色定义
### 员工端角色
- **市场人员** (role_type=1): 负责客户线索管理、市场推广
- **教练** (role_type=2): 负责课程教学、学员管理
- **销售人员** (role_type=3): 负责客户转化、订单管理
### 会员端角色
- **学员** (user_type=member): 查看课程、作业、个人信息
- **家长** (user_type=parent): 管理孩子信息、课程、服务等
## 📊 页面权限配置表
### 🚪 登录认证模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/student/login/login` | 登录页面 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/student/login/forgot` | 找回密码 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
### 📚 学员模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/student/index/index` | 学员首页 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/index/job_list` | 作业列表 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/index/work_details` | 作业详情 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/index/physical_examination` | 体测数据 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/timetable/index` | 课表主页 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/timetable/info` | 课表详情 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/timetable/list` | 场馆列表 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/my/my` | 个人中心 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| `/pages/student/my/my_coach` | 我的教练 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/my/my_members` | 我的成员 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/my/lesson_consumption` | 课时消耗 | 登录验证 | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| `/pages/student/my/personal_data` | 个人资料 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| `/pages/student/my/set_up` | 设置 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| `/pages/student/my/update_pass` | 修改密码 | 登录验证 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
### 👨‍🏫 教练模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/coach/home/index` | 教练首页 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/job/add` | 发布作业 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/job/list` | 全部作业 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/course/list` | 课表管理 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/course/info` | 课时详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/course/info_list` | 课时详情列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/class/list` | 班级管理 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/class/info` | 班级详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/student/student_list` | 我的学员 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/student/student_detail` | 学员详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/student/info` | 学员信息 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/student/work_details` | 作业任务 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/student/physical_examination` | 体测数据 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/schedule/schedule_table` | 课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/schedule/add_schedule` | 添加课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/schedule/adjust_course` | 调整课程安排 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/schedule/sign_in` | 课程点名 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/schedule/schedule_detail` | 课程详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/index` | 个人中心 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `/pages/coach/my/info` | 个人资料 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `/pages/coach/my/set_up` | 设置 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `/pages/coach/my/update_pass` | 修改密码 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `/pages/coach/my/arrival_statistics` | 到课统计 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/due_soon` | 即将到期 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/schooling_statistics` | 授课统计 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/teaching_management` | 教研管理列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/teaching_management_info` | 文章详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/gotake_exam` | 考试 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/exam_results` | 考试结果 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/my_attendance` | 我的考勤 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/service_list` | 服务列表 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `/pages/coach/my/service_detail` | 服务详情 | 角色验证 | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
### 💼 市场模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/market/home/index` | 市场首页 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `/pages/market/index/index` | 销售首页 | 角色验证 | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/index` | 线索管理 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/clue_info` | 客户详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/order_list` | 订单列表 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/add_clues` | 添加客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/edit_clues` | 编辑客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/edit_clues_log` | 修改记录 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/new_task` | 新增任务 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/writing_followUp` | 添加跟进 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/class_arrangement` | 课程安排 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/class_arrangement_detail` | 课程安排详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/clue/clue_table` | 数据统计 | 角色验证 | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/course/course_detail` | 课程详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/data/index` | 数据中心 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `/pages/market/data/statistics` | 市场数据统计 | 角色验证 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `/pages/market/reimbursement/list` | 报销列表 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/reimbursement/add` | 新增报销 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/reimbursement/detail` | 报销详情 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/my/index` | 个人中心 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `/pages/market/my/info` | 个人资料 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `/pages/market/my/set_up` | 设置 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `/pages/market/my/update_pass` | 修改密码 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `/pages/market/my/signed_client_list` | 已签客户 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/my/my_data` | 我的数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/my/dept_data` | 部门数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/my/campus_data` | 校区数据 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `/pages/market/my/firm_info` | 企业信息 | 角色验证 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
### 👥 家长模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/parent/user-info/index` | 用户信息 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/user-info/child-detail` | 孩子详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/courses/index` | 课程管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/courses/course-detail` | 课程详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/materials/index` | 教学资料 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/materials/material-detail` | 资料详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/services/index` | 服务管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/services/service-detail` | 服务详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/orders/index` | 订单管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/orders/order-detail` | 订单详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/messages/index` | 消息管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/messages/message-detail` | 消息详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/contracts/index` | 合同管理 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `/pages/parent/contracts/contract-detail` | 合同详情 | 角色验证 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
### 🛠️ 公共模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/common/privacy_agreement` | 隐私协议 | 公开访问 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/article_info` | 文章详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/feedback` | 意见反馈 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/my_message` | 我的消息 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/im_chat_info` | 聊天详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/sys_msg_list` | 系统消息 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| `/pages/common/contract_list` | 订单列表 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `/pages/common/my_attendance` | 我的考勤 | 登录验证 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| `/pages/common/personnel/add_personnel` | 新员工信息填写 | 管理员权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `/pages/common/contract/my_contract` | 我的合同 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `/pages/common/contract/contract_detail` | 合同详情 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `/pages/common/contract/contract_sign` | 合同签订 | 登录验证 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### 📋 教务模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/academic/home/index` | 教务首页 | 管理员权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
### 🧪 测试模块
| 页面路径 | 页面名称 | 权限等级 | 市场 | 教练 | 销售 | 学员 | 家长 | 建议纳入权限管理 |
|---------|---------|---------|-----|-----|-----|-----|-----|-----------------|
| `/pages/demo/mock-demo` | Mock数据演示 | 开发权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `/pages/demo/dict_optimization` | 字典优化 | 开发权限 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
## 🎯 权限管理建议
### 🔥 高优先级页面(建议优先纳入权限管理)
#### 核心业务功能
- **线索管理模块**: 客户信息、跟进记录、订单管理
- **课程教学模块**: 课表管理、作业管理、学员管理
- **数据统计模块**: 各类业务数据和报表
- **财务模块**: 报销管理、合同管理
#### 敏感数据页面
- **个人信息查看**: 学员详情、教练信息、客户资料
- **体测数据**: 学员体能测试结果
- **考勤统计**: 教练和学员的考勤记录
- **教研管理**: 教学资料和考试内容
### 🔶 中优先级页面(建议选择性纳入)
#### 查看类功能
- **课表查看**: 学员课程表、教练课程安排
- **消息通知**: 系统消息、聊天功能
- **服务管理**: 各类服务项目
### 🔷 低优先级页面(可不纳入权限管理)
#### 通用功能
- **个人设置**: 个人资料、密码修改、系统设置
- **公共信息**: 隐私协议、文章详情、意见反馈
## ⚙️ 技术实现建议
### 1. 路由守卫实现
```javascript
// router/permission.js
const routePermissions = {
'/pages/market/clue/index': ['market', 'sales'],
'/pages/coach/home/index': ['coach'],
'/pages/student/index/index': ['member', 'parent'],
// ...更多路由权限配置
}
function checkPermission(route, userRole) {
const requiredRoles = routePermissions[route]
return !requiredRoles || requiredRoles.includes(userRole)
}
```
### 2. 菜单动态生成
```javascript
// menu/menuConfig.js
const menuConfig = {
market: [
{ path: '/pages/market/home/index', name: '首页', icon: 'home' },
{ path: '/pages/market/clue/index', name: '线索管理', icon: 'clue' },
// ...市场人员菜单
],
coach: [
{ path: '/pages/coach/home/index', name: '首页', icon: 'home' },
{ path: '/pages/coach/course/list', name: '课表管理', icon: 'course' },
// ...教练菜单
],
// ...其他角色菜单
}
```
### 3. API权限验证
```php
// middleware/AuthMiddleware.php
public function handle($request, $next, $role = null) {
$user = $this->getUser($request);
if ($role && !$this->hasRole($user, $role)) {
return $this->unauthorizedResponse();
}
return $next($request);
}
```
## 📝 配置建议总结
### ✅ 强烈建议纳入权限管理的页面(58个)
1. **线索管理**: 11个页面(客户信息、跟进、订单等)
2. **教练功能**: 22个页面(课程、学员、作业等)
3. **家长功能**: 14个页面(孩子管理、服务等)
4. **数据统计**: 8个页面(各类业务报表)
5. **其他业务**: 3个页面(教务、人员、考勤等)
### 🔶 可选择纳入权限管理的页面(15个)
1. **学员查看**: 7个页面(课表、作业、体测等)
2. **公共业务**: 8个页面(合同、消息、服务等)
### ❌ 建议不纳入权限管理的页面(25个)
1. **登录认证**: 2个页面
2. **个人设置**: 12个页面
3. **公共信息**: 6个页面
4. **开发测试**: 2个页面
5. **基础功能**: 3个页面
总计98个页面中,建议将**58个核心业务页面**纳入权限管理系统,确保数据安全和角色隔离的同时,保持系统的易用性和维护性。
Loading…
Cancel
Save