diff --git a/.gitignore b/.gitignore index b8fb0403..4f7e0922 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,9 @@ examples PRPs INITIAL.md CLAUDE.local.md +uniapp/TASK.md +uniapp/PLANNING.md +niucloud/TASK.md +niucloud/PLANNING.md diff --git a/doc/xx校区周&月综合报表.xls b/doc/xx校区周&月综合报表.xls deleted file mode 100644 index 9a215c3e..00000000 Binary files a/doc/xx校区周&月综合报表.xls and /dev/null differ diff --git a/doc/xx校区周&月转化表.xls b/doc/xx校区周&月转化表.xls deleted file mode 100644 index 079a0ee8..00000000 Binary files a/doc/xx校区周&月转化表.xls and /dev/null differ diff --git a/doc/副本xx校区周&月综合报表.xlsx b/doc/副本xx校区周&月综合报表.xlsx new file mode 100644 index 00000000..1b94bfb1 Binary files /dev/null and b/doc/副本xx校区周&月综合报表.xlsx differ diff --git a/doc/副本xx校区周&月转化表.xlsx b/doc/副本xx校区周&月转化表.xlsx new file mode 100644 index 00000000..f4009187 Binary files /dev/null and b/doc/副本xx校区周&月转化表.xlsx differ diff --git a/doc/副本各校区月&年综合报表.xlsx b/doc/副本各校区月&年综合报表.xlsx new file mode 100644 index 00000000..a0b146b4 Binary files /dev/null and b/doc/副本各校区月&年综合报表.xlsx differ diff --git a/doc/副本各校区月&年转化汇总表.xlsx b/doc/副本各校区月&年转化汇总表.xlsx new file mode 100644 index 00000000..ab73a431 Binary files /dev/null and b/doc/副本各校区月&年转化汇总表.xlsx differ diff --git a/doc/私教学员课程协议(1).doc b/doc/副本月卡体能课学员课程协议(1).docx similarity index 66% rename from doc/私教学员课程协议(1).doc rename to doc/副本月卡体能课学员课程协议(1).docx index 571f82a4..a5fedc0b 100644 Binary files a/doc/私教学员课程协议(1).doc and b/doc/副本月卡体能课学员课程协议(1).docx differ diff --git a/doc/课程协议—月卡篮球(1).doc b/doc/副本私教学员课程协议(1).docx similarity index 64% rename from doc/课程协议—月卡篮球(1).doc rename to doc/副本私教学员课程协议(1).docx index cf8c60b4..54721dab 100644 Binary files a/doc/课程协议—月卡篮球(1).doc and b/doc/副本私教学员课程协议(1).docx differ diff --git a/doc/(时间卡)体能课学员课程协议.doc b/doc/副本续费月卡体能课学员课程协议.docx similarity index 66% rename from doc/(时间卡)体能课学员课程协议.doc rename to doc/副本续费月卡体能课学员课程协议.docx index b8318b5f..b1118d73 100644 Binary files a/doc/(时间卡)体能课学员课程协议.doc and b/doc/副本续费月卡体能课学员课程协议.docx differ diff --git a/doc/副本课程协议—月卡篮球(1).docx b/doc/副本课程协议—月卡篮球(1).docx index dfd99098..6d1ee5bb 100644 Binary files a/doc/副本课程协议—月卡篮球(1).docx and b/doc/副本课程协议—月卡篮球(1).docx differ diff --git a/doc/副本课程协议—月卡篮球(2).docx b/doc/副本课程协议—月卡篮球(2).docx index 20fbb619..a3c6bfc4 100644 Binary files a/doc/副本课程协议—月卡篮球(2).docx and b/doc/副本课程协议—月卡篮球(2).docx differ diff --git a/doc/副本(时间卡)体能课学员课程协议.docx b/doc/副本(时间卡)体能课学员课程协议.docx index acf72c14..969e784c 100644 Binary files a/doc/副本(时间卡)体能课学员课程协议.docx and b/doc/副本(时间卡)体能课学员课程协议.docx differ diff --git a/doc/各校区月&年综合报表.xls b/doc/各校区月&年综合报表.xls deleted file mode 100644 index 41f77ee6..00000000 Binary files a/doc/各校区月&年综合报表.xls and /dev/null differ diff --git a/doc/各校区月&年转化汇总表.xls b/doc/各校区月&年转化汇总表.xls deleted file mode 100644 index 84118c9b..00000000 Binary files a/doc/各校区月&年转化汇总表.xls and /dev/null differ diff --git a/doc/各校区综合数据管理系统需求文档.md b/doc/各校区综合数据管理系统需求文档.md new file mode 100644 index 00000000..3ee30778 --- /dev/null +++ b/doc/各校区综合数据管理系统需求文档.md @@ -0,0 +1,393 @@ +# 各校区综合数据管理系统需求文档 + +## 文档信息 +- **生成时间**: 2025-07-29 00:19:30 +- **数据来源**: 副本各校区月&年综合报表.xlsx +- **文档版本**: v1.0 +- **项目类型**: 教育培训机构多校区数据管理系统 + +## 1. 项目背景与现状 + +### 1.1 业务背景 +该教育培训机构拥有多个校区,需要对各校区的经营数据进行统一管理和分析。当前使用Excel进行手工统计,存在数据分散、统计效率低、易出错等问题。 + +### 1.2 校区分布 +目前包含以下校区: +- **吾悦校区** +- **国兴校区** +- **国贸校区** +- **远大校区** +- **龙湖校区** + +### 1.3 现状问题 +- 数据录入依赖手工Excel操作,效率低下 +- 各校区数据分散,难以进行横向对比分析 +- 缺乏实时数据监控和预警机制 +- 报表生成耗时,影响决策效率 + +## 2. 业务需求分析 + +### 2.1 数据维度分析 + +#### 2.1.1 时间维度 +系统需要支持以下时间维度的数据统计: +- 1月 +- 2月 +- 3月 +- 4月 +- 5月 +- 6月 +- 7月 +- 8月 +- 9月 +- 10月 +- 11月 +- 12月 +- 年度合计 + +#### 2.1.2 校区维度 +支持多校区数据管理: +- 吾悦校区 +- 国兴校区 +- 国贸校区 +- 远大校区 +- 龙湖校区 + +### 2.2 核心业务模块 + +#### 2.2.1 资源管理模块 +**功能描述**: 管理资源管理相关业务数据 + +**核心指标**: +- 线下资源 +- 邀约数 +- 一访到访 +- 一访成交 +- 二访到访 +- 二访成交 +- 成交率 +- 线上资源 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + +#### 2.2.2 销售业绩模块 +**功能描述**: 管理销售业绩相关业务数据 + +**核心指标**: +- 成交总课时 +- 成交总金额 +- 均单价 +- (净)成交总单数 +- (净)成交总课时 +- (净)成交总金额 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + +#### 2.2.3 转介绍管理模块 +**功能描述**: 管理转介绍管理相关业务数据 + +**核心指标**: +- 转介绍 +- 转介绍资源数 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + +#### 2.2.4 续费管理模块 +**功能描述**: 管理续费管理相关业务数据 + +**核心指标**: +- 续费 +- 月应续费人数 +- 续费人数(本月) +- 续费人数(次月) +- 老卡续费人数 +- 总续费数 +- 月卡续费率 +- 续费总课时 +- 续费总金额 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + +#### 2.2.5 学员异动模块 +**功能描述**: 管理学员异动相关业务数据 + +**核心指标**: +- 异动 +- 转出人数 +- 转出金额 +- 转入人数 +- 转入金额 +- 退费人数 +- 退费金额 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + +#### 2.2.6 经营分析模块 +**功能描述**: 管理经营分析相关业务数据 + +**核心指标**: +- 经营汇总 +- 任务(月) +- 成本(月) +- (净)任务完成率 +- (净)成本完成率 + +**业务流程**: +1. 数据录入:相关指标数据录入 +2. 数据统计:自动计算汇总数据 +3. 数据分析:趋势分析和对比分析 +4. 报表生成:生成相关业务报表 + + +## 3. 功能需求 + +### 3.1 数据录入功能 +- **多校区数据录入**: 支持各校区独立录入数据 +- **批量导入**: 支持Excel文件批量导入 +- **数据校验**: 自动校验数据合理性和完整性 +- **权限控制**: 校区管理员只能录入本校区数据 + +### 3.2 数据统计功能 +- **实时统计**: 数据录入后自动更新统计结果 +- **多维度统计**: 按时间、校区、业务类型等维度统计 +- **自动计算**: 自动计算转化率、完成率等衍生指标 +- **数据汇总**: 自动生成各校区汇总数据 + +### 3.3 数据分析功能 +- **趋势分析**: 各指标的时间趋势分析 +- **对比分析**: 校区间横向对比、同期对比 +- **转化漏斗**: 从资源到成交的完整转化漏斗 +- **异常预警**: 数据异常自动预警 + +### 3.4 报表展示功能 +- **综合仪表盘**: 核心指标实时展示 +- **校区报表**: 单校区详细报表 +- **对比报表**: 多校区对比报表 +- **导出功能**: 支持PDF、Excel格式导出 + +## 4. 技术实现方案 + +### 4.1 系统架构 +- **前端**: Vue.js 3 + Element Plus + ECharts +- **后端**: Node.js + Express + TypeScript +- **数据库**: MySQL 8.0 +- **缓存**: Redis +- **部署**: Docker + Nginx + +### 4.2 数据库设计 + +#### 4.2.1 核心数据表设计 +**校区表 (campus)** +- id: 主键 +- name: 校区名称 +- code: 校区编码 +- status: 状态 +- created_at: 创建时间 +- updated_at: 更新时间 + +**业务数据表 (business_data)** +- id: 主键 +- campus_id: 校区ID +- year: 年份 +- month: 月份 +- metric_category: 指标分类 +- metric_name: 指标名称 +- metric_value: 指标值 +- created_at: 创建时间 +- updated_at: 更新时间 + +**计算指标表 (calculated_metrics)** +- id: 主键 +- campus_id: 校区ID +- year: 年份 +- month: 月份 +- conversion_rate: 成交率 +- renewal_rate: 续费率 +- avg_price: 均单价 +- task_completion_rate: 任务完成率 +- cost_completion_rate: 成本完成率 + +### 4.3 关键算法 + +#### 4.3.1 转化率计算 +- 成交率 = (一访成交 + 二访成交) / (一访到访 + 二访到访) × 100% +- 续费率 = 续费人数(本月) / 月应续费人数 × 100% +- 均单价 = 成交总金额 / 成交总单数 + +#### 4.3.2 数据聚合规则 +- 年度数据 = 各月度数据累加或平均 +- 校区对比 = 同期数据横向对比 +- 趋势分析 = 时间序列数据分析 + +## 5. 界面设计要求 + +### 5.1 主要页面 +1. **数据录入页面**: 表格式录入,支持快速录入和批量导入 +2. **综合仪表盘**: 核心指标卡片式展示,图表可视化 +3. **校区对比页面**: 多校区数据对比分析 +4. **报表管理页面**: 报表生成、导出、历史查看 +5. **系统管理页面**: 用户管理、权限管理、校区管理 + +### 5.2 用户体验要求 +- **响应式设计**: 支持PC端和平板端使用 +- **操作便捷**: 减少点击次数,支持键盘快捷操作 +- **数据可视化**: 图表展示直观清晰 +- **加载性能**: 页面加载时间 < 3秒 + +## 6. 权限与安全 + +### 6.1 角色权限设计 +- **超级管理员**: 全系统权限,可管理所有校区数据 +- **校区管理员**: 只能管理本校区数据,查看对比报表 +- **数据录入员**: 只能录入本校区数据,无查看权限 +- **数据分析师**: 只读权限,可查看所有数据和报表 + +### 6.2 数据安全要求 +- **数据加密**: 敏感数据加密存储 +- **操作日志**: 记录所有数据变更操作 +- **数据备份**: 每日自动备份,保留30天 +- **访问控制**: IP白名单,异常登录预警 + +## 7. 性能要求 + +### 7.1 响应时间要求 +- 页面加载时间 ≤ 3秒 +- 数据查询响应时间 ≤ 2秒 +- 报表生成时间 ≤ 5秒 +- 数据导出时间 ≤ 10秒 + +### 7.2 并发性能要求 +- 支持50个并发用户同时使用 +- 数据库连接池优化 +- 缓存策略优化关键查询 + +## 8. 验收标准 + +### 8.1 功能验收 +- [ ] 各校区数据录入功能正常 +- [ ] 数据统计计算准确无误 +- [ ] 报表展示美观清晰 +- [ ] 权限控制有效 +- [ ] 数据导入导出功能正常 + +### 8.2 性能验收 +- [ ] 响应时间满足要求 +- [ ] 并发性能达标 +- [ ] 数据安全可靠 +- [ ] 系统稳定性良好 + +## 9. 风险评估与应对 + +### 9.1 技术风险 +**风险**: Excel数据迁移可能存在数据丢失 +**应对**: 制定详细的数据迁移方案,多次测试验证 + +**风险**: 多校区数据同步可能存在延迟 +**应对**: 采用事务机制保证数据一致性 + +### 9.2 业务风险 +**风险**: 用户可能不适应新系统操作 +**应对**: 提供详细培训和操作手册 + +**风险**: 数据录入错误可能影响决策 +**应对**: 增加数据校验规则和审核流程 + +## 10. 实施计划 + +### 10.1 开发阶段 (6周) +- **第1周**: 需求确认、系统设计 +- **第2周**: 数据库设计、后端架构搭建 +- **第3-4周**: 后端API开发、前端页面开发 +- **第5周**: 功能集成、测试 +- **第6周**: 用户培训、上线部署 + +### 10.2 验收测试 +- 单元测试覆盖率 > 80% +- 集成测试通过率 100% +- 用户验收测试通过 +- 性能测试达标 + +## 11. 后续优化建议 + +### 11.1 功能扩展 +- **移动端应用**: 开发移动端APP,支持移动办公 +- **智能分析**: 引入AI算法,提供数据预测和建议 +- **第三方集成**: 与财务系统、CRM系统集成 +- **自动化报表**: 定时自动生成和发送报表 + +### 11.2 技术优化 +- **微服务架构**: 系统模块化,提高可维护性 +- **大数据分析**: 引入大数据技术,支持更复杂的分析 +- **实时数据流**: 实现数据实时同步和分析 +- **云原生部署**: 采用云原生技术,提高系统弹性 + +--- + +**备注**: +1. 本需求文档基于Excel数据结构分析生成,具体业务规则需与业务方进一步确认 +2. 技术方案需根据实际技术栈和团队能力调整 +3. 项目实施过程中需重点关注数据准确性和业务连续性 + +## 附录:业务指标详细说明 + +### A.1 资源管理指标 +- **线下资源**: 通过线下渠道获得的潜在客户数量 +- **线上资源**: 通过线上渠道获得的潜在客户数量 +- **邀约数**: 成功邀约到访的客户数量 +- **一访到访**: 首次到访的客户数量 +- **一访成交**: 首次到访即成交的客户数量 +- **二访到访**: 二次到访的客户数量 +- **二访成交**: 二次到访成交的客户数量 +- **成交率**: 总成交数/总到访数的比例 + +### A.2 销售业绩指标 +- **成交总课时**: 所有成交订单的课时总和 +- **成交总金额**: 所有成交订单的金额总和 +- **均单价**: 平均每单成交金额 +- **(净)成交总单数**: 扣除退费后的实际成交单数 +- **(净)成交总课时**: 扣除退费后的实际成交课时 +- **(净)成交总金额**: 扣除退费后的实际成交金额 + +### A.3 续费管理指标 +- **月应续费人数**: 当月应该续费的学员数量 +- **续费人数(本月)**: 当月实际续费的学员数量 +- **续费人数(次月)**: 延期到次月续费的学员数量 +- **老卡续费人数**: 使用老卡续费的学员数量 +- **总续费数**: 所有续费的总数量 +- **月卡续费率**: 当月续费率 +- **续费总课时**: 续费的总课时数 +- **续费总金额**: 续费的总金额 + +### A.4 学员异动指标 +- **转出人数**: 转出到其他校区的学员数量 +- **转出金额**: 转出学员对应的金额 +- **转入人数**: 从其他校区转入的学员数量 +- **转入金额**: 转入学员对应的金额 +- **退费人数**: 申请退费的学员数量 +- **退费金额**: 退费的总金额 + +### A.5 经营分析指标 +- **任务(月)**: 月度业绩任务目标 +- **成本(月)**: 月度运营成本 +- **(净)任务完成率**: 净业绩/任务目标的完成比例 +- **(净)成本完成率**: 实际成本/预算成本的比例 diff --git a/doc/数据统计系统需求文档.md b/doc/数据统计系统需求文档.md new file mode 100644 index 00000000..daf19baa --- /dev/null +++ b/doc/数据统计系统需求文档.md @@ -0,0 +1,251 @@ +# 教育培训机构数据统计分析系统需求文档 + +## 文档信息 +- **生成时间**: 2025-07-28 23:03:52 +- **数据来源**: 各校区月&年转化汇总表.xlsx +- **文档版本**: v1.0 + +## 1. 项目背景 + +基于现有Excel报表数据分析,该教育培训机构需要一个数字化的数据统计分析系统,用于替代手工Excel报表,实现数据的自动化统计、分析和可视化展示。 + +### 1.1 现状分析 +- 当前使用Excel手工统计各校区数据 +- 包含13个工作表:1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月等 +- 数据维度复杂,包含时间、校区、业务指标等多个维度 + +## 2. 业务需求分析 + +### 2.1 数据维度分析 + +#### 时间维度 +- 1月 +- 2月 +- 3月 +- 4月 +- 5月 +- 6月 +- 7月 +- 8月 +- 9月 +- 10月 +- 11月 +- 12月 +- 年度合计 + +#### 业务分类 +- 资源 + +#### 核心指标 +- 线下资源 +- 邀约数 +- 一访到访 +- 一访成交 +- 二访到访 +- 二访成交 +- 成交率 +- 线上资源 + +### 2.2 核心业务流程 + +#### 2.2.1 资源管理流程 +1. **线下资源管理** + - 邀约数统计 + - 一访到访率跟踪 + - 一访成交率分析 + - 二访到访率跟踪 + - 二访成交率分析 + +2. **线上资源管理** + - 线上资源数量统计 + - 线上转化率分析 + +#### 2.2.2 销售转化流程 +1. **成交管理** + - 成交总课时统计 + - 成交总金额统计 + - 均单价计算 + - 成交率分析 + +2. **转介绍管理** + - 转介绍资源数统计 + - 转介绍转化率分析 + +#### 2.2.3 续费管理流程 +1. **续费统计** + - 月应续费人数 + - 续费人数(本月/次月) + - 老卡续费人数 + - 总续费数 + +2. **续费分析** + - 月卡续费率 + - 续费总课时 + - 续费总金额 + +## 3. 功能需求 + +### 3.1 数据录入模块 +- **手工录入**: 支持按日、周、月维度录入数据 +- **批量导入**: 支持Excel文件批量导入 +- **数据校验**: 自动校验数据完整性和合理性 + +### 3.2 数据统计模块 +- **实时统计**: 自动计算各项指标 +- **多维度统计**: 支持按时间、校区、业务类型等维度统计 +- **自动汇总**: 自动生成周报、月报、年报 + +### 3.3 数据分析模块 +- **趋势分析**: 各指标的时间趋势分析 +- **对比分析**: 校区间、时期间对比分析 +- **转化漏斗**: 从邀约到成交的转化漏斗分析 + +### 3.4 报表展示模块 +- **仪表盘**: 核心指标实时展示 +- **图表展示**: 柱状图、折线图、饼图等多种图表 +- **报表导出**: 支持PDF、Excel格式导出 + +## 4. 技术实现方案 + +### 4.1 系统架构 +- **前端**: Vue.js + Element UI +- **后端**: Node.js/Python + Express/FastAPI +- **数据库**: MySQL/PostgreSQL +- **缓存**: Redis + +### 4.2 数据模型设计 + +#### 4.2.1 核心实体 +```sql +-- 校区表 +CREATE TABLE campus ( + id INT PRIMARY KEY, + name VARCHAR(100), + code VARCHAR(50), + status TINYINT +); + +-- 统计数据表 +CREATE TABLE statistics_data ( + id INT PRIMARY KEY, + campus_id INT, + date DATE, + period_type ENUM('daily', 'weekly', 'monthly', 'yearly'), + metric_type VARCHAR(50), + metric_value DECIMAL(10,2), + created_at TIMESTAMP +); +``` + +### 4.3 关键算法 + +#### 4.3.1 转化率计算 +``` +成交率 = 成交人数 / 到访人数 * 100% +续费率 = 续费人数 / 应续费人数 * 100% +``` + +#### 4.3.2 数据聚合规则 +- 日数据 → 周数据:按自然周聚合 +- 周数据 → 月数据:按自然月聚合 +- 月数据 → 年数据:按自然年聚合 + +## 5. 界面设计要求 + +### 5.1 主要页面 +1. **数据录入页面**: 表格式录入界面,支持快速录入 +2. **统计分析页面**: 多维度查询和分析界面 +3. **报表展示页面**: 图表和表格混合展示 +4. **系统管理页面**: 用户、权限、校区管理 + +### 5.2 用户体验要求 +- 响应式设计,支持PC和移动端 +- 操作简单直观,减少学习成本 +- 数据加载快速,支持分页和懒加载 + +## 6. 数据安全要求 + +### 6.1 权限控制 +- 角色权限管理:超级管理员、校区管理员、数据录入员 +- 数据权限隔离:校区间数据隔离 +- 操作日志记录:记录所有数据变更操作 + +### 6.2 数据备份 +- 定期数据备份 +- 数据恢复机制 +- 异地备份策略 + +## 7. 性能要求 + +### 7.1 响应时间 +- 页面加载时间 < 3秒 +- 数据查询响应时间 < 2秒 +- 报表生成时间 < 5秒 + +### 7.2 并发要求 +- 支持100个并发用户 +- 数据库连接池优化 +- 缓存策略优化 + +## 8. 验收标准 + +### 8.1 功能验收 +- [ ] 数据录入功能完整可用 +- [ ] 统计计算准确无误 +- [ ] 报表展示美观清晰 +- [ ] 权限控制有效 + +### 8.2 性能验收 +- [ ] 响应时间满足要求 +- [ ] 并发性能达标 +- [ ] 数据安全可靠 + +## 9. 风险评估 + +### 9.1 技术风险 +- **数据迁移风险**: Excel数据迁移可能存在数据丢失 +- **性能风险**: 大量历史数据可能影响查询性能 +- **兼容性风险**: 不同浏览器兼容性问题 + +### 9.2 业务风险 +- **用户接受度**: 用户可能不适应新系统 +- **数据准确性**: 自动计算可能与手工计算存在差异 +- **业务连续性**: 系统切换期间业务连续性保障 + +## 10. 实施计划 + +### 10.1 开发阶段 +1. **需求确认** (1周) +2. **系统设计** (1周) +3. **数据库设计** (3天) +4. **后端开发** (2周) +5. **前端开发** (2周) +6. **集成测试** (1周) +7. **用户培训** (3天) +8. **上线部署** (2天) + +### 10.2 验收测试 +- 单元测试覆盖率 > 80% +- 集成测试通过率 100% +- 用户验收测试通过 + +## 11. 后续优化建议 + +### 11.1 功能扩展 +- 移动端APP开发 +- 数据预测分析功能 +- 智能报表推荐 +- 第三方系统集成 + +### 11.2 技术优化 +- 微服务架构改造 +- 大数据分析平台集成 +- AI智能分析功能 +- 实时数据流处理 + +--- + +**注意事项**: +1. 本需求文档基于Excel数据结构分析生成,具体业务规则需要与业务方进一步确认 +2. 技术实现方案需要根据实际技术栈和团队能力进行调整 +3. 项目实施过程中需要密切关注数据准确性和业务连续性 diff --git a/doc/月卡体能课学员课程协议(1).doc b/doc/月卡体能课学员课程协议(1).doc deleted file mode 100644 index 883afda5..00000000 Binary files a/doc/月卡体能课学员课程协议(1).doc and /dev/null differ diff --git a/doc/续费月卡体能课学员课程协议.doc b/doc/续费月卡体能课学员课程协议.doc deleted file mode 100644 index 83de1e98..00000000 Binary files a/doc/续费月卡体能课学员课程协议.doc and /dev/null differ diff --git a/doc/课程协议—月卡篮球(2).doc b/doc/课程协议—月卡篮球(2).doc deleted file mode 100644 index ea4bb27c..00000000 Binary files a/doc/课程协议—月卡篮球(2).doc and /dev/null differ diff --git a/niucloud/PLANNING.md b/niucloud/PLANNING.md new file mode 100644 index 00000000..0519e472 --- /dev/null +++ b/niucloud/PLANNING.md @@ -0,0 +1,406 @@ +# PHP后端开发规划 + +## 已完成接口 + +### 1. 学员课程信息接口 + +**描述**: 获取学员课程信息,用于前端CourseInfoCard组件使用 + +**接口**: `GET /api/getStudentCourseInfo` + +**请求参数**: +| 参数 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| resource_id | int | 是 | 资源ID | +| member_id | string | 否 | 会员ID | +| student_id | int | 否 | 学员ID(优先级高于member_id) | + +**响应示例**: +```json +{ + "code": 1, + "message": "success", + "data": [ + { + "id": 1, + "course_name": "篮球课", + "total_count": 24, + "used_count": 8, + "formal_hours": 20, + "gift_hours": 4, + "used_formal_hours": 6, + "used_gift_hours": 2, + "leave_count": 1, + "start_date": "2024-01-01", + "end_date": "2024-12-31", + "expiry_date": "2024-06-30", + "status": "active", + "course_type": "常规课", + "teacher_name": "张老师", + "course_price": 2880.00, + "class_duration": 90, + "create_time": "2024-01-01 10:00:00", + "remark": "备注信息" + } + ] +} +``` + +**涉及数据表**: +- `school_customer_resources` - 资源表 +- `school_student` - 学员表 +- `school_course` - 课程表 +- `school_student_course` - 学员课程关联表 +- `school_course_schedule` - 课程排期表 +- `school_attendance` - 考勤表 + +**SQL查询逻辑参考**: +```sql +SELECT + sc.id, + sc.course_name, + ssc.total_count, + ssc.used_count, + ssc.formal_hours, + ssc.gift_hours, + ssc.used_formal_hours, + ssc.used_gift_hours, + ssc.leave_count, + ssc.start_date, + ssc.end_date, + ssc.expiry_date, + ssc.status, + sc.course_type, + sp.name as teacher_name, + ssc.course_price, + sc.class_duration, + ssc.create_time, + ssc.remark +FROM school_student_course ssc +LEFT JOIN school_course sc ON ssc.course_id = sc.id +LEFT JOIN school_personnel sp ON sc.teacher_id = sp.id +WHERE ssc.student_id = ? + AND ssc.resource_id = ? + [AND ssc.member_id = ?] +ORDER BY ssc.create_time DESC +``` + +**状态说明**: +- `active`: 正常使用 +- `completed`: 课程完结 +- `expired`: 已过期 +- `pending`: 待激活 + +**业务逻辑**: +1. 优先使用student_id查询,其次使用member_id +2. 计算剩余课时:remaining_count = total_count - used_count +3. 检查课程是否过期,基于expiry_date +4. 返回完整的课程信息及使用情况 +5. 支持多个课程返回 + +**优化建议**: +- 使用Redis缓存常用查询结果 +- 对学员课程查询添加索引优化 +- 考虑分页处理大量数据 + +## 已新增接口 + +### 2. 服务列表接口 + +**描述**: 获取学员服务记录列表 + +**接口**: `GET /api/xy/service/list` + +**请求参数**: +| 参数 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| student_id | int | 是 | 学员ID | + +**响应示例**: +```json +{ + "code": 1, + "message": "操作成功", + "data": [ + { + "id": 1, + "service_name": "测试服务", + "preview_image_url": "图片URL", + "description": "服务描述", + "service_type": "服务类型", + "status": "active", + "logs": [ + { + "id": 1, + "status": 1, + "service_content": "服务内容", + "service_staff": "教练姓名", + "service_time": "服务时间", + "duration": "持续时间", + "customer_feedback": "客户反馈", + "service_rating": 5, + "remark": "备注", + "course_name": "课程名称", + "updated_at": "更新时间" + } + ], + "total_count": 3, + "completed_count": 3 + } + ] +} +``` + +### 3. 体测记录接口(增删改查) + +**描述**: 体测记录的完整CRUD操作 + +**接口列表**: +- `GET /api/xy/physicalTest` - 获取体测记录列表 +- `GET /api/xy/physicalTest/info` - 获取体测记录详情 +- `POST /api/xy/physicalTest/add` - 添加体测记录 +- `POST /api/xy/physicalTest/edit` - 编辑体测记录 +- `POST /api/xy/physicalTest/delete` - 删除体测记录 + +**数据表结构**: +```sql +CREATE TABLE `school_physical_test` ( +`id` int NOT NULL AUTO_INCREMENT COMMENT '体测编号', +`resource_id` int NOT NULL COMMENT '资源ID', +`student_id` int DEFAULT NULL COMMENT '学员ID', +`age` int NOT NULL DEFAULT '0' COMMENT '学员年龄', +`height` decimal(5,2) NOT NULL COMMENT '身高', +`weight` decimal(5,2) NOT NULL COMMENT '体重', +`coach_id` int DEFAULT NULL COMMENT '教练ID', +`seated_forward_bend` decimal(5,2) DEFAULT NULL COMMENT '坐位体前屈', +`sit_ups` decimal(5,2) DEFAULT NULL COMMENT '仰卧起坐', +`push_ups` decimal(5,2) DEFAULT NULL COMMENT '俯卧撑', +`flamingo_balance` decimal(5,2) DEFAULT NULL COMMENT '单脚站立', +`thirty_sec_jump` decimal(5,2) DEFAULT NULL COMMENT '30秒跳绳', +`standing_long_jump` decimal(5,2) DEFAULT NULL COMMENT '立定跳远', +`agility_run` decimal(5,2) DEFAULT NULL COMMENT '敏捷跑', +`balance_beam` decimal(5,2) DEFAULT NULL COMMENT '平衡木', +`tennis_throw` decimal(5,2) DEFAULT NULL COMMENT '网球掷远', +`ten_meter_shuttle_run` decimal(5,2) DEFAULT NULL COMMENT '10米折返跑', +`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', +`physical_test_report` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '体测报告附件(多文件)', +PRIMARY KEY (`id`) USING BTREE +) +``` + +### 4. 学习计划接口(基于自定义表单) + +**描述**: 基于diy_form自定义表单的学习计划管理系统 + +**接口列表**: +- `GET /api/xy/studyPlan` - 获取学习计划列表 +- `GET /api/xy/studyPlan/info` - 获取学习计划详情 +- `POST /api/xy/studyPlan/add` - 添加学习计划 +- `POST /api/xy/studyPlan/edit` - 编辑学习计划 +- `POST /api/xy/studyPlan/delete` - 删除学习计划 +- `POST /api/xy/studyPlan/updateProgress` - 更新学习计划进度 + +**数据表**: 使用diy_form自定义表单系统 +- `school_diy_form` - 表单定义 (type='study_plan') +- `school_diy_form_fields` - 字段定义 +- `school_diy_form_records` - 记录数据 + +**响应数据结构**: +```json +{ + "code": 1, + "message": "操作成功", + "data": [ + { + "id": 1, + "student_id": 1, + "plan_name": "基础体能训练计划", + "plan_content": "针对学员的基础体能进行系统性训练", + "plan_type": "体能训练", + "status": "active", + "progress": 65, + "start_date": "2024-01-15", + "end_date": "2024-03-15", + "target_goals": "提升学员整体体能水平", + "learning_materials": "体能训练器材、训练计划表", + "evaluation_criteria": "体能测试成绩、训练完成度", + "remark": "备注信息", + "create_time": "2024-01-10 14:30:00", + "update_time": "2024-01-10 14:30:00" + } + ] +} +``` + +## 前端集成状态 + +### ✅ 已完成联调的接口: +1. **课程信息** - `getStudentCourseInfo` (完整联调,Mock与API一致) +2. **服务列表** - `getStudentServiceList` (API正常工作) +3. **体测记录** - `xy_physicalTest` (列表API + 增删改查API完整) +4. **学习计划** - `getStudyPlanList` (基于自定义表单的完整CRUD) + +### 🔧 技术实现特点: +- **统一响应格式**: 所有接口使用统一的 `{code: 1, data: [], msg: "操作成功"}` 格式 +- **JWT认证**: 所有接口通过token头进行身份验证 +- **参数验证**: 完整的参数校验和错误处理 +- **数据库事务**: 确保数据一致性 +- **错误处理**: 完善的异常处理和日志记录 + +### 5. 个人资料接口(员工信息管理) + +**描述**: 员工个人资料的查看和编辑功能,涉及基础信息和详细信息两个数据表 + +**接口列表**: +- `GET /api/getPersonnelInfo` - 获取员工基础信息 +- `POST /api/updatePersonnelInfo` - 更新员工基础信息 +- `GET /api/getPersonnelDetailInfo` - 获取员工详细信息 +- `POST /api/updatePersonnelDetailInfo` - 更新员工详细信息 + +#### 基础信息接口 + +**接口**: `GET /api/getPersonnelInfo` + +**请求参数**: +| 参数 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | int | 否 | 员工ID,不传则获取当前登录用户信息 | + +**响应示例**: +```json +{ + "code": 1, + "message": "操作成功", + "data": { + "id": 1, + "name": "张三", + "head_img": "/uploads/avatar/20240101/avatar.jpg", + "gender": 1, + "birthday": "1990-05-15", + "phone": "13800138000", + "email": "zhangsan@example.com", + "wx": "zhangsan_wx", + "address": "北京市朝阳区XXX小区", + "native_place": "山东济南", + "education": "本科", + "profile": "个人简介内容", + "emergency_contact_phone": "13900139000", + "id_card_front": "/uploads/idcard/front.jpg", + "id_card_back": "/uploads/idcard/back.jpg", + "employee_number": "EMP001", + "status": 2, + "account_type": "teacher", + "join_time": "2024-01-01 09:00:00", + "create_time": "2024-01-01 09:00:00", + "update_time": "2024-01-01 09:00:00" + } +} +``` + +**接口**: `POST /api/updatePersonnelInfo` + +**请求参数**: +```json +{ + "id": 1, + "name": "张三", + "head_img": "/uploads/avatar/20240101/avatar.jpg", + "gender": 1, + "birthday": "1990-05-15", + "phone": "13800138000", + "email": "zhangsan@example.com", + "wx": "zhangsan_wx", + "address": "北京市朝阳区XXX小区", + "native_place": "山东济南", + "education": "本科", + "profile": "个人简介内容", + "emergency_contact_phone": "13900139000", + "id_card_front": "/uploads/idcard/front.jpg", + "id_card_back": "/uploads/idcard/back.jpg" +} +``` + +#### 详细信息接口 + +**接口**: `GET /api/getPersonnelDetailInfo` + +**请求参数**: +| 参数 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| person_id | int | 是 | 员工ID | + +**响应示例**: +```json +{ + "code": 1, + "message": "操作成功", + "data": { + "id": 1, + "person_id": 1, + "name": "张三花名", + "store": "朝阳校区", + "ethnicity": "汉族", + "age": 30, + "tenure": "3年2个月", + "regular_date": "2024-07-01", + "is_regular": "是", + "politics": "群众", + "university": "北京大学", + "major": "体育教育", + "graduation_date": "2015-06-30", + "household_place": "山东省济南市", + "household_type": "城镇户口", + "household_address": "山东省济南市历下区XXX街道", + "current_address": "北京市朝阳区XXX小区", + "emergency_contact": "张父", + "emergency_phone": "13900139000", + "marital_status": "已婚", + "bank_card": "6222021234567890123", + "bank_name": "中国银行北京分行", + "contract_expire": "2025-12-31", + "is_rehired": "否", + "remark": "备注信息", + "created_at": "2024-01-01 09:00:00", + "updated_at": "2024-01-01 09:00:00" + } +} +``` + +**接口**: `POST /api/updatePersonnelDetailInfo` + +**请求参数**: 与详细信息响应数据格式相同 + +#### 涉及数据表: +- `school_personnel` - 员工基础信息表 +- `school_personnel_info` - 员工详细信息表 + +#### 字段重复处理: +基于数据库分析,需要删除以下重复字段: +```sql +-- 删除重复字段 +ALTER TABLE school_personnel_info +DROP COLUMN birthday, +DROP COLUMN education, +DROP COLUMN native_place; +``` + +#### 业务逻辑: +1. **只读字段**: `employee_number`(员工编号)、`tenure`(司龄)只能查看不能修改 +2. **司龄计算**: 根据`join_time`自动计算并更新 +3. **图片上传**: 支持头像、身份证正反面图片上传 +4. **数据验证**: + - 手机号格式验证 + - 邮箱格式验证 + - 身份证号码验证 +5. **权限控制**: 员工只能编辑自己的信息 + +#### 前端页面: +- 路径: `/pages/common/profile/personal_info.vue` +- 功能: 查看、编辑员工个人资料 +- 特性: 响应式设计、表单验证、图片上传、分段展示 + +--- + +*最后更新:2024-12-29* +*备注:后续接口开发请及时更新此文档* \ No newline at end of file diff --git a/niucloud/TASK.md b/niucloud/TASK.md new file mode 100644 index 00000000..e69de29b diff --git a/niucloud/app/api/controller/apiController/PhysicalTest.php b/niucloud/app/api/controller/apiController/PhysicalTest.php index 9c7bbe1e..a34a93fe 100644 --- a/niucloud/app/api/controller/apiController/PhysicalTest.php +++ b/niucloud/app/api/controller/apiController/PhysicalTest.php @@ -62,4 +62,101 @@ class PhysicalTest extends BaseApiService return success($res['data']); } + + //添加体测记录 + public function add(Request $request) + { + $data = $request->param(); + + // 验证必填字段 + $required_fields = ['resource_id', 'student_id', 'age', 'height', 'weight']; + foreach ($required_fields as $field) { + if (empty($data[$field])) { + return fail("缺少参数:{$field}"); + } + } + + try { + $res = (new PhysicalTestService())->add($data); + if (!$res['code']) { + return fail($res['msg']); + } + return success($res['data'], '添加成功'); + } catch (\Exception $e) { + return fail('添加失败:' . $e->getMessage()); + } + } + + //编辑体测记录 + public function edit(Request $request) + { + $data = $request->param(); + + if (empty($data['id'])) { + return fail('缺少参数:id'); + } + + try { + $res = (new PhysicalTestService())->edit($data); + if (!$res['code']) { + return fail($res['msg']); + } + return success($res['data'], '修改成功'); + } catch (\Exception $e) { + return fail('修改失败:' . $e->getMessage()); + } + } + + //删除体测记录 + public function delete(Request $request) + { + $id = $request->param('id', ''); + + if (empty($id)) { + return fail('缺少参数:id'); + } + + try { + $res = (new PhysicalTestService())->delete($id); + if (!$res['code']) { + return fail($res['msg']); + } + return success([], '删除成功'); + } catch (\Exception $e) { + return fail('删除失败:' . $e->getMessage()); + } + } + + // 上传PDF文件 + public function uploadPdf(Request $request) + { + try { + $file = $request->file('file'); + if (empty($file)) { + return fail('未找到上传文件'); + } + + // 验证文件类型 + $allowedTypes = ['pdf']; + $extension = strtolower($file->getOriginalExtension()); + if (!in_array($extension, $allowedTypes)) { + return fail('只允许上传PDF文件'); + } + + // 验证文件大小 (最大10MB) + $maxSize = 10 * 1024 * 1024; // 10MB + if ($file->getSize() > $maxSize) { + return fail('文件大小不能超过10MB'); + } + + $res = (new PhysicalTestService())->uploadPdf($file); + if ($res['code']) { + return success($res['data'], '文件上传成功'); + } else { + return fail($res['msg']); + } + } catch (\Exception $e) { + return fail('上传失败:' . $e->getMessage()); + } + } } diff --git a/niucloud/app/api/controller/apiController/StudyPlan.php b/niucloud/app/api/controller/apiController/StudyPlan.php new file mode 100644 index 00000000..b499a914 --- /dev/null +++ b/niucloud/app/api/controller/apiController/StudyPlan.php @@ -0,0 +1,179 @@ +param('student_id', ''); + + if (empty($student_id)) { + return fail('学员ID不能为空'); + } + + try { + $res = (new StudyPlanService())->getList($student_id); + if (!$res['code']) { + return fail($res['msg']); + } + + return success($res['data']); + } catch (\Exception $e) { + return fail('获取学习计划失败:' . $e->getMessage()); + } + } + + /** + * 获取学习计划详情 + * @param Request $request + * @return \think\Response + */ + public function info(Request $request) + { + $record_id = $request->param('record_id', ''); + + if (empty($record_id)) { + return fail('记录ID不能为空'); + } + + try { + $res = (new StudyPlanService())->getInfo($record_id); + if (!$res['code']) { + return fail($res['msg']); + } + + return success($res['data']); + } catch (\Exception $e) { + return fail('获取学习计划详情失败:' . $e->getMessage()); + } + } + + /** + * 添加学习计划 + * @param Request $request + * @return \think\Response + */ + public function add(Request $request) + { + $data = $request->param(); + + // 验证必填字段 + $required_fields = ['student_id', 'plan_name', 'plan_content']; + foreach ($required_fields as $field) { + if (empty($data[$field])) { + return fail("缺少参数:{$field}"); + } + } + + try { + $res = (new StudyPlanService())->add($data); + if (!$res['code']) { + return fail($res['msg']); + } + return success($res['data'], '添加成功'); + } catch (\Exception $e) { + return fail('添加失败:' . $e->getMessage()); + } + } + + /** + * 编辑学习计划 + * @param Request $request + * @return \think\Response + */ + public function edit(Request $request) + { + $data = $request->param(); + + if (empty($data['record_id'])) { + return fail('缺少参数:record_id'); + } + + try { + $res = (new StudyPlanService())->edit($data); + if (!$res['code']) { + return fail($res['msg']); + } + return success($res['data'], '修改成功'); + } catch (\Exception $e) { + return fail('修改失败:' . $e->getMessage()); + } + } + + /** + * 删除学习计划 + * @param Request $request + * @return \think\Response + */ + public function delete(Request $request) + { + $record_id = $request->param('record_id', ''); + + if (empty($record_id)) { + return fail('缺少参数:record_id'); + } + + try { + $res = (new StudyPlanService())->delete($record_id); + if (!$res['code']) { + return fail($res['msg']); + } + return success([], '删除成功'); + } catch (\Exception $e) { + return fail('删除失败:' . $e->getMessage()); + } + } + + /** + * 更新学习计划进度 + * @param Request $request + * @return \think\Response + */ + public function updateProgress(Request $request) + { + $record_id = $request->param('record_id', ''); + $progress = $request->param('progress', 0); + + if (empty($record_id)) { + return fail('缺少参数:record_id'); + } + + if ($progress < 0 || $progress > 100) { + return fail('进度值必须在0-100之间'); + } + + try { + $res = (new StudyPlanService())->updateProgress($record_id, $progress); + if (!$res['code']) { + return fail($res['msg']); + } + return success($res['data'], '更新成功'); + } catch (\Exception $e) { + return fail('更新失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/controller/login/Login.php b/niucloud/app/api/controller/login/Login.php index dee2ecfc..c97966bb 100644 --- a/niucloud/app/api/controller/login/Login.php +++ b/niucloud/app/api/controller/login/Login.php @@ -117,17 +117,17 @@ class Login extends BaseController } //销售教师人员登陆 - public function personnelLogin() - { - $data = $this->request->params([ - ['phone', ''], - ['password', ''], - ['login_type', ''],//登陆类型|1=教练,2=销售 - ]); - //验证码验证 - $result = (new LoginService())->loginByPersonnel($data); - return success($result);//code|1正确 - } +// public function personnelLogin() +// { +// $data = $this->request->params([ +// ['phone', ''], +// ['password', ''], +// ['login_type', ''],//登陆类型|1=教练,2=销售 +// ]); +// //验证码验证 +// $result = (new LoginService())->loginByPersonnel($data); +// return success($result);//code|1正确 +// } public function test(){ $order = new OrderTable(); diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 2c80ee52..518d281d 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -484,12 +484,33 @@ Route::group(function () { Route::get('xy/physicalTest', 'apiController.PhysicalTest/index'); //学生端-体测报告-详情 Route::get('xy/physicalTest/info', 'apiController.PhysicalTest/info'); + //学生端-体测报告-添加 + Route::post('xy/physicalTest/add', 'apiController.PhysicalTest/add'); + //学生端-体测报告-编辑 + Route::post('xy/physicalTest/edit', 'apiController.PhysicalTest/edit'); + //学生端-体测报告-删除 + Route::post('xy/physicalTest/delete', 'apiController.PhysicalTest/delete'); + //学生端-体测报告-PDF上传 + Route::post('xy/physicalTest/uploadPdf', 'apiController.PhysicalTest/uploadPdf'); //学生端-学生课程安排-列表 Route::get('xy/personCourseSchedule', 'apiController.PersonCourseSchedule/index'); //学生端-学生课程安排-详情 Route::get('xy/personCourseSchedule/info', 'apiController.PersonCourseSchedule/info'); + //学生端-学习计划-列表 + Route::get('xy/studyPlan', 'apiController.StudyPlan/index'); + //学生端-学习计划-详情 + Route::get('xy/studyPlan/info', 'apiController.StudyPlan/info'); + //学生端-学习计划-添加 + Route::post('xy/studyPlan/add', 'apiController.StudyPlan/add'); + //学生端-学习计划-编辑 + Route::post('xy/studyPlan/edit', 'apiController.StudyPlan/edit'); + //学生端-学习计划-删除 + Route::post('xy/studyPlan/delete', 'apiController.StudyPlan/delete'); + //学生端-学习计划-更新进度 + Route::post('xy/studyPlan/updateProgress', 'apiController.StudyPlan/updateProgress'); + //学生端-学生课程安排-修改请假状态 Route::post('xy/personCourseSchedule/editStatus', 'apiController.PersonCourseSchedule/editStatus'); //学生端-学生课程安排-获取排课日历 diff --git a/niucloud/app/service/admin/pay/PayService.php b/niucloud/app/service/admin/pay/PayService.php index 12b91a34..eaa0d0a4 100644 --- a/niucloud/app/service/admin/pay/PayService.php +++ b/niucloud/app/service/admin/pay/PayService.php @@ -268,7 +268,20 @@ class PayService extends BaseAdminService 'channel' => '微信扫码支付' ]); - return ['qrcode_url' => getCurrentDomain().$path,'out_trade_no'=>$out_trade_no,'code_url'=> $url['code_url']]; + // 开发环境:读取二维码文件并转换为base64 + $qrcode_base64 = ''; + $full_path = public_path() . $path; + if (file_exists($full_path)) { + $qrcode_content = file_get_contents($full_path); + $qrcode_base64 = 'data:image/png;base64,' . base64_encode($qrcode_content); + } + + return [ + 'qrcode_url' => getCurrentDomain() . $path, // 保留原URL + 'qrcode_base64' => $qrcode_base64, // 新增base64字段 + 'out_trade_no' => $out_trade_no, + 'code_url' => $url['code_url'] + ]; } public function check_payment_status($data) diff --git a/niucloud/app/service/api/apiService/CourseService.php b/niucloud/app/service/api/apiService/CourseService.php index 4a96c3ff..87bfedd6 100644 --- a/niucloud/app/service/api/apiService/CourseService.php +++ b/niucloud/app/service/api/apiService/CourseService.php @@ -447,7 +447,6 @@ class CourseService extends BaseApiService '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(); diff --git a/niucloud/app/service/api/apiService/PhysicalTestService.php b/niucloud/app/service/api/apiService/PhysicalTestService.php index d72df915..1613f5ad 100644 --- a/niucloud/app/service/api/apiService/PhysicalTestService.php +++ b/niucloud/app/service/api/apiService/PhysicalTestService.php @@ -112,4 +112,258 @@ class PhysicalTestService extends BaseApiService return $res; } } + + /** + * 添加体测记录 + * @param array $data + * @return array + */ + public function add(array $data) + { + try { + $model = new PhysicalTest(); + + // 设置基础数据 + $physicalTestData = [ + 'resource_id' => $data['resource_id'], + 'student_id' => $data['student_id'], + 'age' => $data['age'], + 'height' => $data['height'], + 'weight' => $data['weight'], + 'coach_id' => $data['coach_id'] ?? null, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s') + ]; + + // 设置体测项目数据 + $test_items = [ + 'seated_forward_bend', 'sit_ups', 'push_ups', 'flamingo_balance', + 'thirty_sec_jump', 'standing_long_jump', 'agility_run', 'balance_beam', + 'tennis_throw', 'ten_meter_shuttle_run' + ]; + + foreach ($test_items as $item) { + if (isset($data[$item])) { + $physicalTestData[$item] = $data[$item]; + } + } + + // 处理体测报告附件 + if (!empty($data['physical_test_report'])) { + $physicalTestData['physical_test_report'] = is_array($data['physical_test_report']) + ? implode(',', $data['physical_test_report']) + : $data['physical_test_report']; + } + + $result = $model->create($physicalTestData); + + if ($result) { + return [ + 'code' => 1, + 'msg' => '添加成功', + 'data' => $result->toArray() + ]; + } else { + return [ + 'code' => 0, + 'msg' => '添加失败', + 'data' => [] + ]; + } + } catch (\Exception $e) { + return [ + 'code' => 0, + 'msg' => '添加失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } + + /** + * 编辑体测记录 + * @param array $data + * @return array + */ + public function edit(array $data) + { + try { + $model = new PhysicalTest(); + $record = $model->find($data['id']); + + if (!$record) { + return [ + 'code' => 0, + 'msg' => '记录不存在', + 'data' => [] + ]; + } + + // 更新基础数据 + $updateData = [ + 'updated_at' => date('Y-m-d H:i:s') + ]; + + // 允许更新的字段 + $allowed_fields = [ + 'age', 'height', 'weight', 'coach_id', + 'seated_forward_bend', 'sit_ups', 'push_ups', 'flamingo_balance', + 'thirty_sec_jump', 'standing_long_jump', 'agility_run', 'balance_beam', + 'tennis_throw', 'ten_meter_shuttle_run' + ]; + + foreach ($allowed_fields as $field) { + if (isset($data[$field])) { + $updateData[$field] = $data[$field]; + } + } + + // 处理体测报告附件 + if (isset($data['physical_test_report'])) { + $updateData['physical_test_report'] = is_array($data['physical_test_report']) + ? implode(',', $data['physical_test_report']) + : $data['physical_test_report']; + } + + $result = $record->save($updateData); + + if ($result !== false) { + return [ + 'code' => 1, + 'msg' => '修改成功', + 'data' => $record->toArray() + ]; + } else { + return [ + 'code' => 0, + 'msg' => '修改失败', + 'data' => [] + ]; + } + } catch (\Exception $e) { + return [ + 'code' => 0, + 'msg' => '修改失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } + + /** + * 删除体测记录 + * @param int $id + * @return array + */ + public function delete($id) + { + try { + $model = new PhysicalTest(); + $record = $model->find($id); + + if (!$record) { + return [ + 'code' => 0, + 'msg' => '记录不存在', + 'data' => [] + ]; + } + + $result = $record->delete(); + + if ($result) { + return [ + 'code' => 1, + 'msg' => '删除成功', + 'data' => [] + ]; + } else { + return [ + 'code' => 0, + 'msg' => '删除失败', + 'data' => [] + ]; + } + } catch (\Exception $e) { + return [ + 'code' => 0, + 'msg' => '删除失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } + + /** + * 上传PDF文件 + * @param $file + * @return array + */ + public function uploadPdf($file) + { + try { + if (!$file || !$file->isValid()) { + return [ + 'code' => 0, + 'msg' => '上传的文件无效', + 'data' => [] + ]; + } + + // 验证文件类型 + $allowedExtensions = ['pdf']; + $fileExtension = strtolower($file->getOriginalExtension()); + if (!in_array($fileExtension, $allowedExtensions)) { + return [ + 'code' => 0, + 'msg' => '只支持PDF文件格式', + 'data' => [] + ]; + } + + // 验证文件大小(限制10MB) + $maxSize = 10 * 1024 * 1024; // 10MB + if ($file->getSize() > $maxSize) { + return [ + 'code' => 0, + 'msg' => '文件大小不能超过10MB', + 'data' => [] + ]; + } + + // 设置上传目录 + $uploadDir = './uploads/physical_test/pdf/' . date('Y/m/'); + + // 如果目录不存在,创建目录 + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + // 生成唯一文件名 + $fileName = date('YmdHis') . '_' . uniqid() . '.pdf'; + + // 移动文件 + $file->move($uploadDir, $fileName); + + // 构建完整路径 + $fullPath = $uploadDir . $fileName; + $webPath = '/uploads/physical_test/pdf/' . date('Y/m/') . $fileName; + + // 返回文件信息 + return [ + 'code' => 1, + 'msg' => '上传成功', + 'data' => [ + 'file_name' => $fileName, + 'file_path' => $fullPath, + 'file_url' => $webPath, + 'file_size' => filesize($fullPath), + 'upload_time' => date('Y-m-d H:i:s') + ] + ]; + } catch (\Exception $e) { + return [ + 'code' => 0, + 'msg' => '上传失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } } diff --git a/niucloud/app/service/api/apiService/StudyPlanService.php b/niucloud/app/service/api/apiService/StudyPlanService.php new file mode 100644 index 00000000..18c7cd70 --- /dev/null +++ b/niucloud/app/service/api/apiService/StudyPlanService.php @@ -0,0 +1,326 @@ +find(); + if (!$studyPlanForm) { + return ['code' => 0, 'data' => [], 'msg' => '学习计划表单不存在']; + } + + // 查询学习计划记录 + $records = DiyFormRecords::where('form_id', $studyPlanForm->form_id) + ->where('student_id', $studentId) + ->order('create_time desc') + ->select() + ->toArray(); + + $result = []; + foreach ($records as $record) { + $value = is_string($record['value']) ? (json_decode($record['value'], true) ?: []) : (is_array($record['value']) ? $record['value'] : []); + + $result[] = [ + 'id' => $record['record_id'], + 'student_id' => $record['student_id'], + 'plan_name' => $value['plan_name'] ?? '', + 'plan_content' => $value['plan_content'] ?? '', + 'plan_type' => $value['plan_type'] ?? '常规计划', + 'status' => $value['status'] ?? 'active', + 'progress' => (int)($value['progress'] ?? 0), + 'start_date' => $value['start_date'] ?? '', + 'end_date' => $value['end_date'] ?? '', + 'target_goals' => $value['target_goals'] ?? '', + 'learning_materials' => $value['learning_materials'] ?? '', + 'evaluation_criteria' => $value['evaluation_criteria'] ?? '', + 'remark' => $value['remark'] ?? '', + 'create_time' => is_numeric($record['create_time']) ? date('Y-m-d H:i:s', $record['create_time']) : $record['create_time'], + 'update_time' => is_numeric($record['create_time']) ? date('Y-m-d H:i:s', $record['create_time']) : $record['create_time'] + ]; + } + + return ['code' => 1, 'data' => $result, 'msg' => '获取成功']; + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '获取失败:' . $e->getMessage()]; + } + } + + /** + * 获取学习计划详情 + * @param int $recordId 记录ID + * @return array + */ + public function getInfo($recordId) + { + try { + $record = DiyFormRecords::find($recordId); + if (!$record) { + return ['code' => 0, 'data' => [], 'msg' => '记录不存在']; + } + + $value = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []); + + $result = [ + 'id' => $record->record_id, + 'student_id' => $record->student_id, + 'plan_name' => $value['plan_name'] ?? '', + 'plan_content' => $value['plan_content'] ?? '', + 'plan_type' => $value['plan_type'] ?? '常规计划', + 'status' => $value['status'] ?? 'active', + 'progress' => (int)($value['progress'] ?? 0), + 'start_date' => $value['start_date'] ?? '', + 'end_date' => $value['end_date'] ?? '', + 'target_goals' => $value['target_goals'] ?? '', + 'learning_materials' => $value['learning_materials'] ?? '', + 'evaluation_criteria' => $value['evaluation_criteria'] ?? '', + 'remark' => $value['remark'] ?? '', + 'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time, + 'update_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time + ]; + + return ['code' => 1, 'data' => $result, 'msg' => '获取成功']; + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '获取失败:' . $e->getMessage()]; + } + } + + /** + * 添加学习计划 + * @param array $data + * @return array + */ + public function add(array $data) + { + try { + // 获取学习计划表单ID + $studyPlanForm = DiyForm::where('type', 'study_plan')->find(); + if (!$studyPlanForm) { + return ['code' => 0, 'data' => [], 'msg' => '学习计划表单不存在']; + } + + // 构建表单数据 + $formValue = [ + 'plan_name' => $data['plan_name'], + 'plan_content' => $data['plan_content'], + 'plan_type' => $data['plan_type'] ?? '常规计划', + 'status' => $data['status'] ?? 'active', + 'progress' => (int)($data['progress'] ?? 0), + 'start_date' => $data['start_date'] ?? '', + 'end_date' => $data['end_date'] ?? '', + 'target_goals' => $data['target_goals'] ?? '', + 'learning_materials' => $data['learning_materials'] ?? '', + 'evaluation_criteria' => $data['evaluation_criteria'] ?? '', + 'remark' => $data['remark'] ?? '' + ]; + + // 创建记录 + $record = DiyFormRecords::create([ + 'form_id' => $studyPlanForm->form_id, + 'value' => json_encode($formValue), + 'member_id' => $this->member_id ?? 0, + 'relate_id' => 0, + 'student_id' => $data['student_id'], + 'create_time' => time() + ]); + + if ($record) { + $result = [ + 'id' => $record->record_id, + 'student_id' => $record->student_id, + 'plan_name' => $formValue['plan_name'], + 'plan_content' => $formValue['plan_content'], + 'plan_type' => $formValue['plan_type'], + 'status' => $formValue['status'], + 'progress' => $formValue['progress'], + 'start_date' => $formValue['start_date'], + 'end_date' => $formValue['end_date'], + 'target_goals' => $formValue['target_goals'], + 'learning_materials' => $formValue['learning_materials'], + 'evaluation_criteria' => $formValue['evaluation_criteria'], + 'remark' => $formValue['remark'], + 'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time + ]; + + return ['code' => 1, 'data' => $result, 'msg' => '添加成功']; + } else { + return ['code' => 0, 'data' => [], 'msg' => '添加失败']; + } + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '添加失败:' . $e->getMessage()]; + } + } + + /** + * 编辑学习计划 + * @param array $data + * @return array + */ + public function edit(array $data) + { + try { + $record = DiyFormRecords::find($data['record_id']); + if (!$record) { + return ['code' => 0, 'data' => [], 'msg' => '记录不存在']; + } + + // 获取现有数据 + $existingValue = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []); + + // 更新字段 + $formValue = $existingValue; + + $updateFields = [ + 'plan_name', 'plan_content', 'plan_type', 'status', 'progress', + 'start_date', 'end_date', 'target_goals', 'learning_materials', + 'evaluation_criteria', 'remark' + ]; + + foreach ($updateFields as $field) { + if (isset($data[$field])) { + $formValue[$field] = ($field === 'progress') ? (int)$data[$field] : $data[$field]; + } + } + + // 更新记录 + $result = $record->save([ + 'value' => json_encode($formValue) + ]); + + if ($result !== false) { + $resultData = [ + 'id' => $record->record_id, + 'student_id' => $record->student_id, + 'plan_name' => $formValue['plan_name'] ?? '', + 'plan_content' => $formValue['plan_content'] ?? '', + 'plan_type' => $formValue['plan_type'] ?? '常规计划', + 'status' => $formValue['status'] ?? 'active', + 'progress' => (int)($formValue['progress'] ?? 0), + 'start_date' => $formValue['start_date'] ?? '', + 'end_date' => $formValue['end_date'] ?? '', + 'target_goals' => $formValue['target_goals'] ?? '', + 'learning_materials' => $formValue['learning_materials'] ?? '', + 'evaluation_criteria' => $formValue['evaluation_criteria'] ?? '', + 'remark' => $formValue['remark'] ?? '', + 'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time + ]; + + return ['code' => 1, 'data' => $resultData, 'msg' => '修改成功']; + } else { + return ['code' => 0, 'data' => [], 'msg' => '修改失败']; + } + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '修改失败:' . $e->getMessage()]; + } + } + + /** + * 删除学习计划 + * @param int $recordId + * @return array + */ + public function delete($recordId) + { + try { + $record = DiyFormRecords::find($recordId); + if (!$record) { + return ['code' => 0, 'data' => [], 'msg' => '记录不存在']; + } + + $result = $record->delete(); + + if ($result) { + return ['code' => 1, 'data' => [], 'msg' => '删除成功']; + } else { + return ['code' => 0, 'data' => [], 'msg' => '删除失败']; + } + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '删除失败:' . $e->getMessage()]; + } + } + + /** + * 更新学习计划进度 + * @param int $recordId + * @param int $progress + * @return array + */ + public function updateProgress($recordId, $progress) + { + try { + $record = DiyFormRecords::find($recordId); + if (!$record) { + return ['code' => 0, 'data' => [], 'msg' => '记录不存在']; + } + + // 获取现有数据 + $existingValue = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []); + $existingValue['progress'] = (int)$progress; + + // 根据进度自动更新状态 + if ($progress >= 100) { + $existingValue['status'] = 'completed'; + } elseif ($progress > 0) { + $existingValue['status'] = 'in_progress'; + } else { + $existingValue['status'] = 'active'; + } + + // 更新记录 + $result = $record->save([ + 'value' => json_encode($existingValue) + ]); + + if ($result !== false) { + $resultData = [ + 'id' => $record->record_id, + 'progress' => (int)$progress, + 'status' => $existingValue['status'] + ]; + + return ['code' => 1, 'data' => $resultData, 'msg' => '更新成功']; + } else { + return ['code' => 0, 'data' => [], 'msg' => '更新失败']; + } + + } catch (\Exception $e) { + return ['code' => 0, 'data' => [], 'msg' => '更新失败:' . $e->getMessage()]; + } + } +} \ No newline at end of file diff --git a/uniapp/PLANNING.md b/uniapp/PLANNING.md new file mode 100644 index 00000000..08c25eea --- /dev/null +++ b/uniapp/PLANNING.md @@ -0,0 +1,92 @@ +✅ 已完成:uniapp/components/service-list-card/index.vue数据问题 +- 前端:已添加完整的Mock数据支持,组件可正常显示测试数据 +- 后端:PHP后端工程师已成功补充测试数据到数据库 +- API测试:接口 `/xy/service/list?student_id=1` 已验证返回完整数据 +- 最终状态:前端组件可以正常获取和显示真实的服务列表数据 + +API返回数据包含: +- 3种不同类型的服务(体测服务、测试服务、1V1+情感沟通) +- 每个服务包含完整的日志记录、评分、反馈信息 +- 服务状态、教练信息、时间等完整字段 +- 数据结构完全符合前端组件期望格式 +✅ 已完成:uniapp/components/service-list-card/index.vue弹窗无法关闭问题 +- 问题原因:事件绑定和微信小程序兼容性问题 +- 修复内容:添加了事件防冒泡处理,优化微信小程序环境下的事件响应 +- 解决方案:增强了响应式更新机制,添加了调试功能 +- 验证状态:已修复弹窗关闭功能,支持微信小程序环境 + +✅ 已处理:components/course-info-card/index.vue页面的接口需求分析 +- 已完成接口需求分析,包含完整的数据结构和字段定义 +- 已将详细的接口规格写入 niucloud/PLANNING.md 中 +- 等待PHP后端工程师按照规格开发接口 +- 开发完成后需要进行联调测试 +✅ 已修复:课程安排、订单列表、服务列表按钮点击无法打开弹窗问题 +- 问题原因:事件冒泡导致按钮点击事件冒泡到遮罩层,立即触发关闭逻辑 +- 修复方案:为所有触发弹窗的按钮添加 `.stop` 事件修饰符阻止冒泡 +- 涉及组件:主页面、OrderListCard、ServiceListCard等所有弹窗相关按钮 +- 验证状态:弹窗打开和关闭功能均正常,微信小程序环境兼容 + +✅ 已修复:pages/market/clue/clue_info.vue页面所有点击事件失灵问题 +- 问题原因:底部弹窗组件的遮罩层一直存在并覆盖整个视口,拦截所有页面点击 +- 核心问题:遮罩层即使弹窗隐藏时仍然监听点击事件,导致所有操作被误判为遮罩点击 +- 修复方案: + - 为BottomPopup组件添加 `v-if="visible"` 条件渲染 + - 关键元素添加 `@click.stop` 阻止事件冒泡 + - 优化弹窗关闭时的数据重置逻辑 +- 影响范围:修复了页面上所有按钮、标签切换、操作项的点击响应 +- 验证状态:页面交互功能完全恢复正常,微信小程序环境兼容 + +📋 体测记录PDF附件预览功能需求 +**背景**:体测记录弹窗需要支持PDF附件预览功能,允许用户查看体测报告 +**需求描述**: +1. 体测记录数据中需要包含 `pdf_files` 字段,包含附件信息 +2. 需要提供PDF文件预览接口,支持在线预览或下载 +3. 前端需要实现PDF预览功能,兼容微信小程序环境 + +**接口需求**: +1. 获取体测记录接口需要返回PDF附件信息 +2. 新增PDF文件预览/下载接口 + - 接口路径:`/api/fitness/record/pdf/preview` + - 请求方式:GET + - 请求参数: + ```json + { + "file_id": "文件ID", + "record_id": "体测记录ID" + } + ``` + - 响应数据: + ```json + { + "code": 1, + "msg": "success", + "data": { + "file_url": "PDF文件访问URL", + "file_name": "文件名称", + "file_size": 1024000, + "preview_url": "在线预览URL(可选)", + "download_url": "下载URL" + } + } + ``` + +**数据结构扩展**: +体测记录数据中的 `pdf_files` 字段结构: +```json +{ + "pdf_files": [ + { + "id": "文件ID", + "name": "体测报告.pdf", + "size": 1024000, + "upload_time": "2024-01-15 10:30:00", + "file_path": "/uploads/fitness/2024/01/xxx.pdf" + } + ] +} +``` + +**前端实现**: +- 在 FitnessRecordCard 组件中实现PDF预览按钮 +- 支持微信小程序环境下的PDF预览 +- 提供下载功能作为备选方案 \ No newline at end of file diff --git a/uniapp/TASK.md b/uniapp/TASK.md new file mode 100644 index 00000000..14607f34 Binary files /dev/null and b/uniapp/TASK.md differ diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index c28bbb46..dfdb5bd0 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -584,6 +584,43 @@ export default { async xy_physicalTestInfo(data = {}) { return await http.get('/xy/physicalTest/info', data); }, + //学生端-体测报告-添加 + async xy_physicalTestAdd(data = {}) { + return await http.post('/xy/physicalTest/add', data); + }, + //学生端-体测报告-编辑 + async xy_physicalTestEdit(data = {}) { + return await http.post('/xy/physicalTest/edit', data); + }, + //学生端-体测报告-删除 + async xy_physicalTestDelete(data = {}) { + return await http.post('/xy/physicalTest/delete', data); + }, + + //学生端-学习计划-列表 + async getStudyPlanList(data = {}) { + return await http.get('/xy/studyPlan', data); + }, + //学生端-学习计划-详情 + async getStudyPlanInfo(data = {}) { + return await http.get('/xy/studyPlan/info', data); + }, + //学生端-学习计划-添加 + async addStudyPlan(data = {}) { + return await http.post('/xy/studyPlan/add', data); + }, + //学生端-学习计划-编辑 + async editStudyPlan(data = {}) { + return await http.post('/xy/studyPlan/edit', data); + }, + //学生端-学习计划-删除 + async deleteStudyPlan(data = {}) { + return await http.post('/xy/studyPlan/delete', data); + }, + //学生端-学习计划-更新进度 + async updateStudyPlanProgress(data = {}) { + return await http.post('/xy/studyPlan/updateProgress', data); + }, //学生端-学生课程安排-列表 async xy_personCourseSchedule(data = {}) { return await http.get('/xy/personCourseSchedule', data); diff --git a/uniapp/common/config.js b/uniapp/common/config.js index accb611f..7c54f5db 100644 --- a/uniapp/common/config.js +++ b/uniapp/common/config.js @@ -1,6 +1,6 @@ // 环境变量配置 const env = process.env.VUE_APP_ENV || 'development' -const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' || true // 默认启用Mock数据 +const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' || false // 默认禁用Mock优先模式,仅作为回退 const isDebug = process.env.VUE_APP_DEBUG === 'true' || true // 默认启用调试模式 // API配置 - 支持环境变量 diff --git a/uniapp/components/bottom-popup/index.vue b/uniapp/components/bottom-popup/index.vue index 2d565898..f601f990 100644 --- a/uniapp/components/bottom-popup/index.vue +++ b/uniapp/components/bottom-popup/index.vue @@ -1,15 +1,20 @@