From 567e4aabdb11ae3416214ab912e99f9f8e9827c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 5 Nov 2026 19:03:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E6=94=AF=E6=8C=81=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=BD=93=E5=89=8D=E7=94=A8=E6=88=B7=E8=B5=84=E6=96=99?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料 - 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败 - 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示 - 修正环境变量 .env.development 中 API URL 配置为生产地址 - 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数 - 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致 - 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能 - 各新增功能支持表单验证、操作反馈和数据加载优化 --- .env.development | 4 +- .workbuddy/memory/2026-07-22.md | 34 ++ .workbuddy/memory/2026-07-23.md | 6 + .workbuddy/memory/MEMORY.md | 5 + ...ession_7ab6728b66664f295b2b7d1220f273d7.md | 295 ++++++++++++ docs/phase2-P0-delivery.md | 194 ++++++++ docs/phase2-implementation-plan.md | 178 ++++++++ src/api/growth/club/index.ts | 392 ++++++++++++++++ src/api/growth/credit/index.ts | 224 +++++++++ src/api/growth/home/index.ts | 65 +++ src/api/growth/practice/index.ts | 132 ++++++ src/api/growth/qmgc/index.ts | 289 ++++++++++++ src/api/growth/thematic/index.ts | 102 +++++ src/api/growth/volunteer/index.ts | 136 ++++++ src/api/growth/warning/index.ts | 91 ++++ src/api/system/notice/index.ts | 95 ++++ src/api/system/user/index.ts | 14 + src/components/MultiFileUpload/index.vue | 130 ++++++ src/utils/ws-url.ts | 30 ++ src/views/club/activity/components/edit.vue | 190 ++++++++ src/views/club/activity/index.vue | 203 +++++++++ src/views/club/activityReview/index.vue | 202 +++++++++ src/views/club/annualReview/index.vue | 52 +++ src/views/club/award/index.vue | 52 +++ src/views/club/club/components/edit.vue | 171 +++++++ src/views/club/club/index.vue | 297 ++++++++++++ src/views/club/venue/index.vue | 425 ++++++++++++++++++ src/views/club/venueReview/index.vue | 202 +++++++++ src/views/club/volunteerCertificate/index.vue | 173 +++++++ .../club/volunteerHour/components/edit.vue | 142 ++++++ .../club/volunteerHour/components/import.vue | 88 ++++ src/views/club/volunteerHour/index.vue | 391 ++++++++++++++++ src/views/club/volunteerReview/index.vue | 208 +++++++++ src/views/growth/credit/components/edit.vue | 148 ++++++ src/views/growth/credit/components/import.vue | 89 ++++ src/views/growth/credit/index.vue | 312 +++++++++++++ src/views/growth/creditWarning/index.vue | 278 ++++++++++++ src/views/growth/myHome/index.vue | 336 ++++++++++++++ src/views/growth/portrait/index.vue | 52 +++ .../growth/practiceCredit/components/edit.vue | 155 +++++++ src/views/growth/practiceCredit/index.vue | 375 ++++++++++++++++ src/views/growth/recommend/index.vue | 52 +++ src/views/gxmu/components/reviewStatusTag.vue | 7 +- src/views/gxmu/expertPool/index.vue | 52 +++ src/views/gxmu/expertReview/index.vue | 52 +++ src/views/gxmu/qmgcScore/index.vue | 224 +++++++++ .../gxmu/qmgcStudent/components/edit.vue | 163 +++++++ src/views/gxmu/qmgcStudent/index.vue | 404 +++++++++++++++++ src/views/gxmu/qmgcTask/index.vue | 256 +++++++++++ src/views/gxmu/teachingDesign/index.vue | 51 +++ src/views/gxmu/thematicCourse/index.vue | 293 ++++++++++++ src/views/gxmu/themeDayDesign/index.vue | 51 +++ src/views/gxmu/utils/review.ts | 31 +- src/views/system/messageLog/index.vue | 131 ++++++ src/views/system/messageTemplate/index.vue | 203 +++++++++ src/views/user/profile/index.vue | 34 +- 56 files changed, 8941 insertions(+), 20 deletions(-) create mode 100644 .workbuddy/memory/2026-07-22.md create mode 100644 .workbuddy/memory/2026-07-23.md create mode 100644 .workbuddy/memory/MEMORY.md create mode 100644 .zcode/plans/plan-session_7ab6728b66664f295b2b7d1220f273d7.md create mode 100644 docs/phase2-P0-delivery.md create mode 100644 docs/phase2-implementation-plan.md create mode 100644 src/api/growth/club/index.ts create mode 100644 src/api/growth/credit/index.ts create mode 100644 src/api/growth/home/index.ts create mode 100644 src/api/growth/practice/index.ts create mode 100644 src/api/growth/qmgc/index.ts create mode 100644 src/api/growth/thematic/index.ts create mode 100644 src/api/growth/volunteer/index.ts create mode 100644 src/api/growth/warning/index.ts create mode 100644 src/api/system/notice/index.ts create mode 100644 src/components/MultiFileUpload/index.vue create mode 100644 src/utils/ws-url.ts create mode 100644 src/views/club/activity/components/edit.vue create mode 100644 src/views/club/activity/index.vue create mode 100644 src/views/club/activityReview/index.vue create mode 100644 src/views/club/annualReview/index.vue create mode 100644 src/views/club/award/index.vue create mode 100644 src/views/club/club/components/edit.vue create mode 100644 src/views/club/club/index.vue create mode 100644 src/views/club/venue/index.vue create mode 100644 src/views/club/venueReview/index.vue create mode 100644 src/views/club/volunteerCertificate/index.vue create mode 100644 src/views/club/volunteerHour/components/edit.vue create mode 100644 src/views/club/volunteerHour/components/import.vue create mode 100644 src/views/club/volunteerHour/index.vue create mode 100644 src/views/club/volunteerReview/index.vue create mode 100644 src/views/growth/credit/components/edit.vue create mode 100644 src/views/growth/credit/components/import.vue create mode 100644 src/views/growth/credit/index.vue create mode 100644 src/views/growth/creditWarning/index.vue create mode 100644 src/views/growth/myHome/index.vue create mode 100644 src/views/growth/portrait/index.vue create mode 100644 src/views/growth/practiceCredit/components/edit.vue create mode 100644 src/views/growth/practiceCredit/index.vue create mode 100644 src/views/growth/recommend/index.vue create mode 100644 src/views/gxmu/expertPool/index.vue create mode 100644 src/views/gxmu/expertReview/index.vue create mode 100644 src/views/gxmu/qmgcScore/index.vue create mode 100644 src/views/gxmu/qmgcStudent/components/edit.vue create mode 100644 src/views/gxmu/qmgcStudent/index.vue create mode 100644 src/views/gxmu/qmgcTask/index.vue create mode 100644 src/views/gxmu/teachingDesign/index.vue create mode 100644 src/views/gxmu/thematicCourse/index.vue create mode 100644 src/views/gxmu/themeDayDesign/index.vue create mode 100644 src/views/system/messageLog/index.vue create mode 100644 src/views/system/messageTemplate/index.vue diff --git a/.env.development b/.env.development index 302408a..2951c31 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ VITE_APP_NAME=后台管理系统 -VITE_API_URL=http://127.0.0.1:38927/api -#VITE_API_URL=https://tuanwei-api.gxwebsoft.com/api +#VITE_API_URL=http://127.0.0.1:38927/api +VITE_API_URL=https://twai-api.gxmu.edu.cn/api diff --git a/.workbuddy/memory/2026-07-22.md b/.workbuddy/memory/2026-07-22.md new file mode 100644 index 0000000..a6684bd --- /dev/null +++ b/.workbuddy/memory/2026-07-22.md @@ -0,0 +1,34 @@ +# 2026-07-22 + +## VITE_API_URL 部署配置分析 + +**项目**:tuanwei-admin(广西医科大学团委智能化平台 - AI团宝) + +### 三个 .env 文件当前配置 +- `.env`(默认):`VITE_API_URL=/api`(相对路径,配合 nginx 反代) +- `.env.development`:`VITE_API_URL=http://127.0.0.1:10051/api`(开发) +- `.env.production`:`VITE_API_URL=http://twai-api/api`(生产,依赖 nginx upstream 或 docker DNS) + +### 部署后登录卡住:根因不是 API URL,而是数据库 + +**后端错误**: +``` +java.sql.SQLSyntaxErrorException: Table 'tuanwei.sys_user' doesn't exist +``` + +**API URL 连通性实测**: +- ✅ `http://10.129.0.125:38927/api` 通,CORS 已正确配置(Access-Control-Allow-Origin: http://10.129.0.125) +- ❌ `http://127.0.0.1:38927/api` Connection refused(端口不在 127.0.0.1 上监听,Spring Boot 绑定可能是 0.0.0.0:38927) +- ✅ `http://twai-api/api` 取决于 nginx upstream 是否配置 + +**`docs/sql/` 三个文件均无 sys_user 建表脚本**: +- gxmu_college_class.sql +- gxmu_mysql.sql +- gxmu_declare_migration.sql + +按用户记忆约定:sys_user 应归属 gxwebsoft_core 库(租户 ID 10582),但后端当前直查 tuanwei 库导致表不存在。 + +### 后续待办 +1. 确认后端数据源(是否在 yml 里配了多数据源 / 切库) +2. 在后端让 sys_user 走 gxwebsoft_core 库,或在 tuanwei 库建 sys_user 表 +3. 生产环境推荐 VITE_API_URL=/api + nginx 反代,避免 CORS 和端口暴露 diff --git a/.workbuddy/memory/2026-07-23.md b/.workbuddy/memory/2026-07-23.md new file mode 100644 index 0000000..f1b058b --- /dev/null +++ b/.workbuddy/memory/2026-07-23.md @@ -0,0 +1,6 @@ +# 2026-07-23 工作日志 + +## 图片压缩需求澄清(项目搞错) +- 用户在 tuanwei-admin 提「列表页图片压缩宽度太大」需求,但 `getCompressedImageUrl` 实际在 **website-admin/app/utils/image.ts**,tuanwei-admin 源码里无此函数(曾搜遍 src / node_modules / git 历史均不存在)。 +- 曾误在 tuanwei-admin 新建 `getCompressedImageUrl` 并改 3 个列表页(cms/article、sys/sysSwiper、system/user),已全部 `git checkout` 还原(`.env` 的改动非我所为,保留)。 +- 真实修复在 website-admin:见 website-admin/.workbuddy/memory。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md new file mode 100644 index 0000000..b411f02 --- /dev/null +++ b/.workbuddy/memory/MEMORY.md @@ -0,0 +1,5 @@ +# 项目长期记忆(tuanwei-admin) + +## 图片压缩相关需求不在此仓库 +- `getCompressedImageUrl` 等 OSS 图片压缩逻辑**不在 tuanwei-admin**(源码/依赖/git 历史均无),实际在 **website-admin/app/utils/image.ts**。 +- 若再收到「列表页图片压缩 / 改小宽度」类需求,直接去 website-admin 改,不要动 tuanwei-admin。 diff --git a/.zcode/plans/plan-session_7ab6728b66664f295b2b7d1220f273d7.md b/.zcode/plans/plan-session_7ab6728b66664f295b2b7d1220f273d7.md new file mode 100644 index 0000000..76c9bab --- /dev/null +++ b/.zcode/plans/plan-session_7ab6728b66664f295b2b7d1220f273d7.md @@ -0,0 +1,295 @@ +# 团委智能化平台二期实施方案 + +> 依据《副本二期功能--定稿0802.docx》(4 大模块 26 项功能),结合 tuanwei-admin(Vue3)与 tuanwei-java(Spring Boot)现有底座制定。文档中括号内的疑问已逐条对应到「待确认项」,未确认处按默认假设推进。 + +## 一、总体判断 + +二期不是从零建设。现有系统已经具备四项可复用底座:**申报+审核流引擎**(9 张申报表、三级审核、状态回写)、**学生/教师名册与认领体系**(`gxmu_student.user_id` 是身份主键)、**文件与 Word 导出**(S3 存储、XWPF 直绘、电子签名图片叠加)、**爬虫与异步任务**(jsoup + 定时 + 进度表)。 + +真正缺失的是三块能力,必须新建: +1. **站内消息中心**——后端完全没有 `oa_notice`,但前端顶栏铃铛和 `/user/notice` 页面已经在调 `/oa/notice/*`,属于「前端等后端」。 +2. **业务域**——学分、志愿、社团、场地、青马培养档案、专家评审均无表、无接口。 +3. **OCR / PDF 生成**——无 OCR 依赖;PDF 只能靠服务器 LibreOffice 转换。 + +**结论:全量规划、分三期实施。** 本期(P0)先打通「学生成长主页 + 学分 + 志愿 + 社团场地 + 青马档案 + 消息中心 + 团课库」这条主干,P1 补齐审核类与 AI 类,P2 做画像与数字人。 + +移动端:本机没有 uni-app 工程,`tuanwei-admin` 是纯 PC 后台。**本期只交付 PC,接口按可对外(`/api/open`)设计**,学生端后续独立建工程,不阻塞本期。 + +--- + +## 二、菜单规划 + +原则:**不动现有 12 个一级菜单、不迁移已有子菜单**,避免破坏已配置的角色授权。二期新增 2 个一级菜单,其余能力挂到语义最贴近的现有菜单下。 + +### 新增一级菜单 + +``` +成长中心 /growth (menu_id 14500) +├ 我的成长 /growth/myHome 14501 学生个人成长主页 +├ 学分明细 /growth/credit 14502 第二课堂学分 + 社会实践学分汇总 +├ 社会实践学分 /growth/practiceCredit 14503 申报 + 四级审核 + 教务处模板导出 +├ 学分预警 /growth/creditWarning 14504 缺口预测 + 补分建议 +├ 学生画像 /growth/portrait 14505 多维雷达 + 标签 +└ 推荐内容 /growth/recommend 14506 思政文章/活动推荐 + 后台发布 + +社团与志愿 /club (menu_id 14520) +├ 志愿服务时长 /club/volunteerHour 14521 个人录入 + 桂志愿导入 +├ 志愿时长审核 /club/volunteerReview 14522 二级学院审核 +├ 志愿证明 /club/volunteerCertificate 14523 年度证明生成下载 +├ 社团管理 /club/club 14524 社团 + 成员 + 指导老师 +├ 社团活动 /club/activity 14525 发起 + 材料 +├ 活动审批 /club/activityReview 14526 四级审批 + 公章识别 +├ 场地预约 /club/venue 14527 场地日历 + 预约 +├ 场地审批 /club/venueReview 14528 指导老师 → 校团委 +├ 社团年审 /club/annualReview 14529 指标 + 自评 + 自动取数 + 公示 +└ 社团评优 /club/award 14530 评优申报 + 审核 + 公示 +``` + +### 挂到现有菜单 + +| 现有菜单 | 新增子项 | path | +|---|---|---| +| 数字化团员 14342 | 青马学员档案 | `/gxmu/qmgcStudent` | +| | 培养任务 | `/gxmu/qmgcTask` | +| | 考核评分 | `/gxmu/qmgcScore` | +| | 主题团课库 | `/gxmu/thematicCourse` | +| AI内容创作与宣传助手 14365 | AI辅助教学设计 | `/gxmu/teachingDesign` | +| | AI主题团日设计 | `/gxmu/themeDayDesign` | +| 挑战杯 14372 | 专家评审 | `/gxmu/expertReview` | +| | 专家库 | `/gxmu/expertPool` | +| 系统设置 8574 | 消息模板 | `/system/messageTemplate` | +| | 推送记录 | `/system/messageLog` | + +`青马工程`(14353)保留为**申报入口**,新增的学员档案管培养期,两者不合并。消息中心的用户侧直接复用已有的 `/user/notice`(已隐藏菜单)+ 顶栏铃铛,零改动即可生效。 + +### 不推荐但已备选 + +把「五四评优/学术之星/青马工程」从数字化团员迁到申报系统,结构更清晰,但会动 6 个菜单的 `parent_id` 和全部角色授权,回归风险高。如后续确需重组,单独出一个迁移脚本。 + +--- + +## 三、分期实施计划 + +### P0(本期,可上线的最小闭环) + +| # | 功能 | 对应文档 | 交付物 | +|---|---|---|---| +| 1 | 站内消息中心 | 模块一·4 | `oa_notice` + 记录表 + `/oa/notice/*` 接口 + 审核钩子(提交/通过/驳回/待办)+ 顶栏铃铛生效 | +| 2 | 第二课堂学分 | 模块一·1 | 学分规则表 + 学分记录表 + Excel 导入(模板/校验/失败清单)+ 明细页 | +| 3 | 学生成长主页 | 模块一·3 | 聚合接口 + 简历式页面(基本信息/学分/志愿/活动/申报/荣誉) | +| 4 | 志愿时长 | 模块三·1·2 | 个人录入(盖章附件)+ 桂志愿导入 + 二级学院审核 | +| 5 | 社团管理 | 模块三·3·7 | 社团主档 + 成员 + 指导老师 + 活动发起 + 四级审批 | +| 6 | 场地预约 | 模块三·6 | 场地主档 + 日历视图 + 冲突校验 + 指导老师→校团委审批 | +| 7 | 青马培养档案 | 模块二·1 | 学员主档 + 培养材料提交 + 任务清单 + 任务完成度 | +| 8 | 主题团课库 | 模块二·6 | 爬虫(jsoup + 定时 + upsert)+ 列表/详情/下载 + 标签 | +| 9 | 工程配套 | — | 菜单/按钮/角色授权 SQL、字典、权限码、联调回归 | + +估算 **53 人日**,2 人并行约 **5.5 周**。 + +### P1(第二批) + +社会实践学分申报(四级审核 + 教务处模板导出)、学分预警(规则 + 定时 + 消息)、智能推荐(内容库 + 规则引擎 + 推送)、学生画像(维度聚合 + 雷达图)、社团年审(指标配置 + 自评 + 自动取数 + 公示)、社团评优、年度志愿证明(docx + 电子章 + PDF)、社团年度报告(聚合 + AI 小结 + 导出)、专家评审(专家库 + 任务 + 分配 + 打分 + 汇总)、AI 辅助教学设计与主题团日设计、**审核流并行审批改造**。 + +估算 **63 人日**,2 人并行约 **6.5 周**。 + +### P2(探索期) + +AI 学员学习情况分析、AI 定制培养建议、OCR 公章识别接入、AI 数字人(形象 + 浮窗 + 知识库问答)、移动端接口整理。 + +估算 **23 人日**,约 **2.5 周**。 + +**合计约 139 人日,2 人并行约 14-15 周。** + +--- + +## 四、数据模型(新表) + +统一规范:`id BIGINT AUTO_INCREMENT`、`tenant_id`、`user_id`、`deleted`、`create_time`、`update_time`;附件用 `file_id + file_url` 或 JSON 数组;审批一律复用 `gxmu_review_list(module, pk)`。 + +### 学分 / 成长域 +- `gxmu_credit_rule`:学分类别、项目、分值、认定方式(import/manual/practice)、适用年级 +- `gxmu_credit_record`:`user_id, source(second_class|practice|activity), external_id, category, item_name, score, acquire_time, status, evidence` +- `gxmu_practice_credit_apply`:学年、活动名、类型、起止、角色、内容、附件 JSON +- `gxmu_graduate_requirement`:`year, college_id, major, category, required_score` +- `gxmu_credit_warning`:`user_id, category, gap, level, hit_time, notified` +- `gxmu_student_profile`:画像快照(维度分、标签 JSON、更新时间) +- `gxmu_recommend_content`:类型(article/activity)、标题、封面、关联 ID、标签 +- `gxmu_recommend_delivery`:`user_id, content_id, channel, is_read` + +### 志愿域 +- `gxmu_volunteer_record`:`user_id, source(gz|self), project, hours, service_date, org_name, proof_files JSON, status` +- `gxmu_certificate_template`:模板标题、正文占位符、电子章图片、编号前缀 +- `gxmu_volunteer_certificate`:`user_id, year, total_hours, serial_no, file_id` + +### 社团 / 场地域 +- `gxmu_club`:名称、类别、`college_id`、`advisor_user_id`、社长、等级、状态 +- `gxmu_club_member`:`club_id, user_id, role, join_time, status` +- `gxmu_club_activity`:`club_id, title, start/end, place, scale, budget, attachments JSON, review_status, ocr_status, ocr_org` +- `gxmu_club_annual_metric`:指标名、权重、取数方式(auto/manual)、公式 +- `gxmu_club_annual_review`:`club_id, year, self_score, auto_score, final_score, materials, status` +- `gxmu_club_award_apply`:`club_id, year, award_type, materials, status` +- `gxmu_club_report`:`club_id, year, summary JSON, file_id` +- `gxmu_venue`:名称、楼栋、容量、设备、开放时段、管理员、状态 +- `gxmu_venue_booking`:`venue_id, user_id, club_id, booking_date, start_time, end_time, purpose, status` + +### 青马域 +- `gxmu_qmgc_student`:`user_id, form_id, batch, year, college_id, candidate_no, stage, result` +- `gxmu_qmgc_material`:`student_id, stage, type, title, content, file_id, score` +- `gxmu_qmgc_task`:批次、阶段、标题、类型(online/offline)、跳转方式、截止时间 +- `gxmu_qmgc_task_assign`:`student_id, task_id, status, submit_time, score` +- `gxmu_qmgc_score`:`student_id, dimension, item, score, scorer, comment` +- `gxmu_qmgc_exam` / `gxmu_qmgc_exam_record`:题目 JSON、及格线、作答、得分 + +### 团课 / 专家 / 消息 +- `gxmu_thematic_course`:`title, type(courseware|script|video|case|activity), source_site, source_url, source_key UNIQUE, file_id, content, tags, heat` +- `gxmu_expert`:`user_id, name, college, field_code, level, phone` +- `gxmu_expert_review_task`:`module, declare_id, round, start/end, quota` +- `gxmu_expert_review_assign`:`task_id, expert_id, status` +- `gxmu_expert_review_score`:`assign_id, dimensions JSON, total, comment, submit_time` +- `oa_notice` + `oa_notice_record`:从 `/Users/gxwebsoft/JAVA/com.gxwebsoft.open` 移植,字段对齐前端已有的 `noticeRecordId/type/files/channel/progress` + +### 对现有表的改动(小步、可回滚) +1. `gxmu_review_flow` + `gxmu_review_list` 增加 `parallel_group INT NULL` —— 支持「二级学院团委与党委同时审核」。 +2. `gxmu_review_list` 增加 `attachments TEXT` —— 审核节点上传盖章附件。 +3. `gxmu_declare.module` 若新增「团委科研课题」,同步加枚举(见待确认项)。 + +--- + +## 五、后端设计 + +### 包结构(新增,不动现有 24 个 gxmu 控制器) + +``` +com.gxwebsoft.gxmu.credit/ 学分规则、记录、导入、预警 +com.gxwebsoft.gxmu.qmgc/ 青马学员、任务、评分、考试 +com.gxwebsoft.gxmu.club/ 社团、活动、年审、评优、年度报告 +com.gxwebsoft.gxmu.venue/ 场地与预约 +com.gxwebsoft.gxmu.volunteer/ 志愿时长与证明 +com.gxwebsoft.gxmu.thematic/ 主题团课库与爬虫 +com.gxwebsoft.gxmu.portrait/ 画像与推荐 +com.gxwebsoft.gxmu.expert/ 专家评审 +com.gxwebsoft.notice/ 消息中心(移植 + 渠道) +``` + +每个子包内 `controller/entity/mapper/service/param`,与现有 `gxmu.openplat` 的组织方式一致。 + +### 接口约定 + +- 管理端统一 `/api/gxmu/{domain}/page|list|{id}|save|update|remove|batch`,沿用 `BaseController` + `BaseParam`,前端继续传 `backendAccess=true`。 +- 学生自助统一 `/api/gxmu/{domain}/my/*`,用 `hasOnlyStudentRole()` 收口,只返回本人数据。 +- 审核类不在各域重复造轮子,统一走 `gxmu_review_list`,前端复用 `reviewStatusTag` / `reviewHistoryModal` / `reviewFlowModal`。 +- 消息中心按前端**既有路径** `/api/oa/notice/*` 实现(`page`/`list`/`{id}`/`getUnReadNum`/`save`/`update`/`remove`),顶栏铃铛和 `/user/notice` 不需要改一行代码。 +- 导出走现有模式:Controller 生成文件 → `FileStorageService.storeFile` → `FileRecord` → 返回下载地址。 + +### 关键改造:审核流支持并行节点 + +现状:`ReviewListController.update` 用 `sortNumber + 1` 取下一节点,隐含「一个节点一个人」。青马选拔要求团委与党委**同时审核**,因此: +- 模板按 `level` 分组,同 `level` 的多行属于同一并行组; +- 通过时先检查同组是否还有 `status=0` 的节点,有则不推进; +- 同组全部通过才进入下一 `level`; +- 组内任一驳回是否终止整条流,做成 `ReviewFlowConfig.rejectMode`(任一驳回 / 全部驳回后终止),默认前者。 +- 该改造同时被社会实践学分、社团活动、场地预约复用。 + +### 关键改造:学分引擎 + +不引入规则引擎,用「配置表 + 计算服务」: +1. `CreditRuleService.resolve(userId, year)` 按学院/年级/专业匹配 `gxmu_graduate_requirement`,得到各类别应修学分。 +2. `CreditSummaryService` 汇总 `gxmu_credit_record` 中 `status=有效` 的记录,按类别输出「已修/应修/差额/完成率」。 +3. 预警任务每天跑一次:差额 > 0 且距毕业不足 N 学期 → 生成 `gxmu_credit_warning` + 发站内消息;同一学生同一类别本月只发一次。 +4. 推荐:先做规则匹配(缺分 → 推荐可加分的活动/课程,按标签和学院过滤),AI 排序留到 P2。 + +### 关键技术点 + +- **志愿证明 PDF**:XWPF 直绘(复用 `WxxzxDocxExportUtil` 的 `addPicture` 叠电子章)→ `OpenOfficeUtil.converterToPDF` → 存 `FileRecord`。若服务器无 `soffice`,降级为前端打印(复用 `youthDataCenter.exportPdf` 模式),不阻塞上线。 +- **团课库爬虫**:完全复用 `CrossSchoolActivityCrawlerServiceImpl` 范式(站点配置 + jsoup + `source_key` 唯一键 + `batchUpsert` + 定时任务 + 手动 `/sync`),新增文件下载入库。 +- **AI 扩展**:`DifyProperties.AppKeys` 增加 4 个应用 key;`AiChatType` 增加 `teaching_design / theme_day_design / student_analysis / training_advice`;新增 `/api/ai/{xxx}` 与 `/stream`,沿用 `AiGenerationRecorder` 落历史。 +- **OCR**:不引入本地推理。P1 通过阿里云 OCR HTTP 接口(需开通 + AK),字段预留 `ocr_status/ocr_org/ocr_raw`;未开通时走「人工核验 + 附件留痕」。 +- **消息渠道**:站内信必做;企业微信在 `WxWorkUtil` 增加 `message/send`(token 逻辑已有);短信暂不做。 + +--- + +## 六、前端设计 + +### 目录与页面 + +``` +src/views/growth/ + myHome/ 我的成长(简历式:基本信息/学分/志愿/活动/申报/荣誉) + credit/ 学分明细(类别筛选 + 来源 + 明细表 + 导入) + practiceCredit/ 社会实践学分(申报表单 + 审核状态) + creditWarning/ 学分预警(雷达/进度条/建议) + portrait/ 学生画像 + recommend/ 推荐内容 +src/views/club/ + volunteerHour/ 我的志愿时长(录入 + 导入 + 列表) + volunteerReview/ 志愿时长审核 + volunteerCertificate/ + club/ 社团管理(主档 + 成员抽屉) + activity/ 社团活动 + activityReview/ 活动审批(复用 reviewFlowModal) + venue/ 场地预约(日历 + 预约弹窗) + venueReview/ + annualReview/ 年审(指标 + 自评 + 打分) + award/ +src/views/gxmu/ + qmgcStudent/ qmgcTask/ qmgcScore/ thematicCourse/ + teachingDesign/ themeDayDesign/ + expertReview/ expertPool/ +src/views/system/ + messageTemplate/ messageLog/ +``` + +### 复用清单(不要重写) + +- 列表骨架:`ele-pro-table` + `.page/.ele-body/a-card`,照 `studentRoster`。 +- 导入:`views/gxmu/components/common-import.vue`(模板下载、逐行校验、失败清单)。 +- 附件:`components/attachmentList.vue` + `views/gxmu/utils/attachment.ts`;单文件用 `components/UploadFile`。 +- 审核:`reviewStatusTag` / `reviewHistoryModal` / `reviewFlowModal` / `views/gxmu/utils/review.ts`。 +- 图表:`utils/use-echarts.ts` + `vue-echarts`,照 `youthDataCenter`。 +- 导出:Excel 用 `xlsx`;打印式 PDF 照 `youthDataCenter.exportPdf`;docx 用 `html-docx-js-typescript`。 +- AI 页:复制 `activityOutlineGen` 壳 + `utils/ai-stream.ts` + `utils/ai-result.ts`。 +- 权限:`hasPermission` / `v-permission`;角色用 `hasAnyRole`(`hasRole` 是「全部满足」语义)。 + +### 需要新建的公共件 + +1. `components/MultiFileUpload`:统一多附件上传(现有各表单各写了一套)。 +2. `components/AiGenPageShell`:把「左侧表单 + 右侧流式输出 + 历史入口」抽出,避免再复制 4 个 800-1300 行的页面。 +3. `utils/export.ts`:收敛 Excel/CSV/打印 PDF 的重复代码。 +4. `src/constants/permissions.ts`:集中权限码。 + +### 顺带修的两个既有问题 + +- `views/user/profile` 保存走 `PUT /system/user`(要 `sys:user:update`),学生账号会失败。改为后端已有的 `PUT /system/user/self`,成长主页的「编辑资料」直接受益。 +- `reviewStatusTag` 按 `id` 取最新,`utils/review.ts` 按 `createTime+id`,两处规则不一致。二期接入新审核模块前统一到 `utils/review.ts`。 + +--- + +## 七、实施顺序(建议按此推进) + +1. **基建先行**:消息中心(表 + 移植 + 钩子)+ 菜单/权限 SQL + 字典 + `MultiFileUpload`。 +2. **学历域**:学分规则/记录/导入 → 成长主页聚合 → 明细页。 +3. **志愿域**:录入/导入 → 审核 → 证明(证明依赖 LibreOffice,放最后)。 +4. **社团域**:社团主档/成员 → 活动 → 四级审批 → 场地日历 → 场地审批。 +5. **青马域**:学员档案 → 任务 → 材料 → (P1)评分/考试。 +6. **团课库**:建表 → 爬虫 → 列表/详情。 +7. **P1**:学分预警/推荐/画像 → 社会实践 → 社团年审/评优/年报 → 专家评审 → AI 教学设计/团日设计 → 审核流并行改造(此改造需在青马选拔上线前完成,实际应提前到 P0 末尾)。 +8. **P2**:AI 学员分析/培养建议 → OCR → 数字人。 + +每批交付都包含:建表 SQL(可重复执行)、菜单/按钮 SQL、后端接口、前端页面、联调、回归。第一步先在仓库落地 `docs/phase2-implementation-plan.md`(本方案)和 `docs/sql/phase2_*.sql`。 + +--- + +## 八、待你确认的业务细节(有默认值,确认前按默认推进) + +| 事项 | 默认假设 | 影响 | +|---|---|---| +| 第二课堂 / 桂志愿 是否有 API | 先 Excel 导入,`source/external_id` 预留,后续换数据源 | P0 导入模板字段 | +| 学分规则与毕业要求 | 按「学院 + 年级 + 专业 + 类别」配应修学分,由管理员在后台维护 | 预警准确性 | +| 青马评分标准/任务清单 | 评分项和权重做成可配置表,任务分线上(可跳转)与线下(仅勾选) | P1 评分页 | +| 社团年审量化指标 | 做成指标配置表,活动数/参与人次/稿件数可自动取数,其余人工 | P1 年审 | +| 指导老师是单人还是多人 | 社团主档存 1 名主指导,审批节点支持多人并行 | 审批流 | +| 志愿证明模板与电子章 | 需要你提供模板文件与印章图片;未提供时用占位版 | P1 证明 | +| 「团委科研课题」是否新申报模块 | 视为新模块,新增 `gxmu_research_project` 表并加入申报枚举 | 专家评审范围 | +| 数字人形象 | 先用团委现有卡通形象,不做 3D | P2 | + +其中前三项不确认就无法开工;其余可边做边定。**只要前三项给出结论,P0 可以立即启动。** diff --git a/docs/phase2-P0-delivery.md b/docs/phase2-P0-delivery.md new file mode 100644 index 0000000..288a33c --- /dev/null +++ b/docs/phase2-P0-delivery.md @@ -0,0 +1,194 @@ +# 二期 P0 交付说明 + +## 一、本次交付范围 + +按《副本二期功能--定稿0802.docx》规划,P0 阶段交付以下 8 个功能模块,全部已完成前后端联调验证。 + +| # | 功能 | 状态 | 后端接口前缀 | 前端页面 | +| --- | --- | --- | --- | --- | +| 1 | 站内消息中心 | 已完成 | `/api/oa/notice`、`/api/oa/notice-template` | 复用既有顶栏铃铛 + `/user/notice`;新增模板与推送记录页 | +| 2 | 第二课堂学分 | 已完成 | `/api/gxmu/credit` | `/growth/credit` | +| 3 | 学生成长主页 | 已完成 | `/api/gxmu/growth/my` | `/growth/myHome` | +| 4 | 志愿服务时长 | 已完成 | `/api/gxmu/volunteer` | `/club/volunteerHour`、`/club/volunteerReview`、`/club/volunteerCertificate` | +| 5 | 社团管理 | 已完成 | `/api/gxmu/club` | `/club/club`、`/club/activity`、`/club/activityReview` | +| 6 | 场地预约 | 已完成 | `/api/gxmu/venue` | `/club/venue`、`/club/venueReview` | +| 7 | 青马培养档案 | 已完成 | `/api/gxmu/qmgc-student` | `/gxmu/qmgcStudent`、`/gxmu/qmgcTask`、`/gxmu/qmgcScore` | +| 8 | 主题团课库 | 已完成 | `/api/gxmu/thematic-course` | `/gxmu/thematicCourse` | + +P1/P2 的 10 个功能已建菜单与占位页(写明实施依赖),不会出现菜单 404: + +社会实践学分、学分预警、学生画像、推荐内容、社团年审、社团评优、AI 辅助教学设计、AI 主题团日设计、专家评审、专家库。 + +## 二、数据库部署 + +在 `tuanwei-java/sql/` 下按顺序执行(全部可重复执行): + +```bash +mysql -h -P -u -p tuanwei < sql/20261105_phase2_01_growth_credit.sql +mysql ... < sql/20261105_phase2_02_volunteer.sql +mysql ... < sql/20261105_phase2_03_club_venue.sql +mysql ... < sql/20261105_phase2_04_qmgc.sql +mysql ... < sql/20261105_phase2_05_thematic_course.sql +mysql ... < sql/20261105_phase2_06_notice.sql +mysql ... < sql/20261105_phase2_07_review_parallel.sql +mysql ... < sql/20261105_phase2_08_menu_permission.sql +mysql ... < sql/20261105_phase2_09_dict.sql +``` + +| 脚本 | 内容 | 是否含初始数据 | +| --- | --- | --- | +| 01 | 学分规则/记录/社会实践/毕业要求/预警/画像/推荐(8 张表) | 6 条毕业学分要求 | +| 02 | 志愿时长/证明模板/已生成证明(3 张表) | 无 | +| 03 | 社团/成员/活动/年审/评优/年报/场地/预约(8 张表) | 无 | +| 04 | 青马学员/材料/任务/任务分配/评分/考试(7 张表) | 无 | +| 05 | 主题团课库(1 张表) | 无 | +| 06 | 站内消息 + 消息模板(2 张表) | 10 条消息模板 | +| 07 | 审核流并行节点改造(ALTER 4 处) | 回填存量 parallel_group | +| 08 | 菜单 + 按钮权限 + 角色授权(62 条菜单/按钮) | 超管全量、学生部分 | +| 09 | 字典(学分类别、社团类别) | 11 条字典项 | + +**注意**:脚本 07 会修改 `gxmu_review_flow` / `gxmu_review_list` / `gxmu_review_flow_config` 三张生产表,只是新增列并回填,不影响存量串行审核逻辑。 + +## 三、后端改动清单 + +### 新增包 + +``` +com.gxwebsoft.notice 消息中心(Notice / NoticeTemplate / 2 个 Controller / 2 个 Mapper) +com.gxwebsoft.gxmu.credit 学分域(规则、记录、汇总、导入) +com.gxwebsoft.gxmu.volunteer 志愿服务(录入、导入、审核、汇总) +com.gxwebsoft.gxmu.club 社团(主档、成员、活动) +com.gxwebsoft.gxmu.venue 场地与预约(含时间段冲突校验) +com.gxwebsoft.gxmu.qmgcpro 青马培养档案(学员、材料、任务、评分) +com.gxwebsoft.gxmu.thematic 主题团课库(含 jsoup 爬虫) +com.gxwebsoft.gxmu.growth 成长主页聚合 +``` + +### 关键实现说明 + +1. **消息中心**:路径与前端既有约定完全一致(`/api/oa/notice`),顶栏铃铛和 `/user/notice` 无需改代码即可生效。查询默认按当前登录用户收口,学生只能看自己的消息。 +2. **学分汇总**:应修学分来自 `gxmu_graduate_requirement`(学院专属优先于「不限」),已修来自 `gxmu_credit_record`;按类别输出已修/应修/差额/完成率。 +3. **场地冲突校验**:同场地 + 同日期 + 状态非驳回,时间段半开区间重叠即拒绝;审核通过前会二次校验,避免并发下重复通过。 +4. **成长主页聚合**:串起 `sys_user` + `gxmu_student` + `gxmu_tygl_form` + 学分 + 志愿 + 社团 + 青马;任一未建表时静默跳过,不影响其他区块。 +5. **消息推送**:志愿提交/审核、场地预约/审核已接入模板推送,失败不阻塞主业务。 +6. **团课爬虫**:站点通过 `gxmu.thematic-course.sites` 配置(`名称|列表页|条目选择器|标题选择器|链接选择器`,多个站点换行分隔),未配置时只支持手工维护。 + +### 新增配置项 + +```yaml +gxmu: + thematic-course: + max-per-site: 50 # 单站点单轮抓取上限 + sites: | # 可选,格式见上 +``` + +## 四、前端改动清单 + +### 新增目录 + +``` +src/api/growth/{credit,volunteer,home,club,qmgc,thematic}/ +src/api/system/notice/ +src/components/MultiFileUpload/ 统一多附件上传(JSON 数组与后端契约) +src/views/growth/{myHome,credit,practiceCredit,creditWarning,portrait,recommend}/ +src/views/club/{volunteerHour,volunteerReview,volunteerCertificate,club,activity,activityReview,venue,venueReview,annualReview,award}/ +src/views/gxmu/{qmgcStudent,qmgcTask,qmgcScore,thematicCourse,teachingDesign,themeDayDesign,expertReview,expertPool}/ +src/views/system/{messageTemplate,messageLog}/ +``` + +### 复用而非重写 + +- 列表页统一 `ele-pro-table` + `.page/.ele-body/a-card`,与 `studentRoster` 一致。 +- 导入复用 `views/gxmu/components/common-import.vue`(模板下载、逐行校验、失败清单)。 +- 附件展示复用 `attachmentList`,审核状态复用 `reviewStatusTag`。 +- 权限判断用 `hasPermission` / `hasAnyRole`(注意 `hasRole` 是「全部满足」语义)。 + +## 五、验证记录 + +### 已执行 + +| 项 | 结果 | +| --- | --- | +| 后端 `mvn -o compile`(JDK 17) | 通过 | +| 前端 `vite build` | 通过 | +| 前端 `vue-tsc`(仅新增文件) | 0 错误 | +| 9 个 SQL 脚本在开发库执行 | 全部成功 | +| 18 个 GET 接口 | 全部 200 / code=0 | +| 社团 → 成员 → 详情(人数联动) | 通过 | +| 场地 → 预约 → 冲突拒绝 → 当日排期 | 通过 | +| 志愿录入(缺附件拒绝)→ 审核 → 汇总 4 小时 | 通过 | +| 学分导入 → 重复导入跳过 → 汇总 0.5/9.0 | 通过 | +| 成长主页聚合(真实学院/班级/团支书数据) | 通过 | +| 志愿审核触发站内消息落库 | 通过 | + +### 验证后已清理 + +测试期间创建的社团、成员、场地、预约、志愿记录、学分记录、站内消息已全部删除,保留毕业要求与消息模板等基础配置。 + +## 六、上线前必做 + +1. **执行 9 个 SQL 脚本**(生产库先备份,尤其脚本 07 改动审核表)。 +2. **给角色授权**:脚本 08 只授了 `superAdmin` 和 `student`,其余角色(校团委、二级学院团委、指导老师)需在「角色管理」里按需勾选二期菜单。 +3. **配置团课抓取站点**(如需自动同步),否则该页只能手工录入。 +4. **确认 Redis 可用**:消息与登录依赖 Redis,本地验证时需先启动。 +5. **学生账号**:需要 `gxmu_student.user_id` 已认领,成长主页才能带出学籍信息;否则只显示账号基础信息。 + +## 七、已知边界 + +| 项 | 说明 | +| --- | --- | +| 志愿证明 PDF | 当前只做数据汇总与提示,模板渲染与电子章在 P1 接入(需学校提供模板与印章图) | +| OCR 公章识别 | 字段已预留(`ocr_status` / `ocr_org` / `ocr_raw`),未接 OCR 服务;活动审批暂由人工核验材料 | +| 审核流并行 | **已实现**,见下方「补充交付」 | +| 学分自动获取 | 第二课堂/桂志愿暂无 API,先走 Excel 导入,`source` + `external_id` 已为后续对接预留 | +| 移动端 | 本期只交付 PC,接口均支持 `/api/open` 对外复用,移动端工程后续独立建设 | + + +## 八、补充交付(P0 收尾追加) + +### 8.1 审核流并行审批 + +青马选拔要求「二级学院团委 + 二级学院党委」同时审核,原审核引擎用 `sortNumber + 1` 推进,隐含「一个节点一个人」。 + +**改动:** + +| 文件 | 改动 | +| --- | --- | +| `entity/ReviewFlow` | 新增 `parallelGroup` | +| `entity/ReviewList` | 新增 `parallelGroup`、`attachments` | +| `entity/ReviewFlowConfig` | 新增 `rejectMode` | +| `service/ReviewProgressService`(新增) | 按并行组推进、幂等创建、组内待审判断 | +| `service/impl/ReviewProgressServiceImpl`(新增) | 上述实现 | +| `service/impl/ReviewFlowStarterServiceImpl` | 发起时创建首个并行组的**全部**节点(原来只建一个) | +| `controller/ReviewListController` | 用 `progressService.advance()` 替换原串行推进 | +| `service/impl/DeclareGuardServiceImpl` | `syncReviewStatus` 改为「任一组驳回过即驳回」,不再只看最后一条 | + +**语义:** + +- `parallelGroup` 相同的节点属同一组,**全部通过**才进入下一组; +- 组内任一驳回按 `reject_mode` 处理,默认 `any`(任一驳回即终止); +- `parallelGroup` 为空的存量数据退化为「sortNumber 即组号」,**保持原串行行为**; +- 推进逻辑幂等:重复点击通过不会重复创建下一组节点。 + +**验证结果(开发库实测):** + +| 场景 | 结果 | +| --- | --- | +| 同组两人,第一人通过 | 不创建下一组(仍有待审)✓ | +| 第二人通过 | 创建第 2 组节点 ✓ | +| 第 2 组驳回 | 不再创建新节点,流程终止 ✓ | +| 末组驳回后申报记录状态 | 回写为「驳回」(原逻辑会误判为通过)✓ | + +### 8.2 修复:9 张申报表缺 `declare_id` 列 + +开发库未执行过 `sql/20261102_add_declare_uniqueness.sql`,导致青马申报页报 `Unknown column 'declare_id'`。已执行该迁移,9 张申报表接口全部恢复 200。 + +**上线提醒**:若生产库也未执行此迁移,请一并补上,否则青马工程、五四评优等申报页会直接报错。 + +### 8.3 修复:个人资料保存 403 + +`views/user/profile` 原来调用 `PUT /system/user`(需 `sys:user:update`),学生账号没有该权限会保存失败。已改为后端已有的 `PUT /system/user/self`,并新增前端封装 `updateSelfUser`。 + +### 8.4 统一最新审核记录排序 + +`reviewStatusTag` 原按 `id` 取最新,`utils/review.ts` 按 `createTime + id`,并行审核下两处可能给出矛盾结论。现已抽出 `getLatestReview()` 统一为「sortNumber → createTime → id」,两个调用方共用。 diff --git a/docs/phase2-implementation-plan.md b/docs/phase2-implementation-plan.md new file mode 100644 index 0000000..8354943 --- /dev/null +++ b/docs/phase2-implementation-plan.md @@ -0,0 +1,178 @@ +# 团委智能化平台二期实施方案 + +> 依据《副本二期功能--定稿0802.docx》(4 大模块 26 项功能)制定。 +> 前端仓库:`tuanwei-admin`(Vue3 + ele-admin-pro);后端仓库:`tuanwei-java`(Spring Boot + MyBatis-Plus)。 + +## 一、现状结论 + +二期的四项底座已经具备,可直接复用: + +| 底座 | 现状 | 可复用点 | +| --- | --- | --- | +| 申报 + 审核流 | `gxmu_declare` / `gxmu_review_flow` / `gxmu_review_flow_config` / `gxmu_review_list`,9 张申报表全部接入 | 三级审核、状态回写、`ReviewFlowStarterService` | +| 学生统一身份 | `gxmu_student.user_id`(开放平台同步 + 认领)、`gxmu_teacher`、`gxmu_tygl_form` | 成长主页身份主键、组织作用域 | +| 文件存储 | `/api/file/upload` + S3 对象存储 + 缩略图 + office 转 PDF | 所有附件、证明材料 | +| 爬虫 + 异步任务 | `CrossSchoolActivityCrawlerServiceImpl`(jsoup + 定时 + upsert)、`OpenplatSyncServiceImpl`(进度表 + 轮询) | 主题团课库、批量导入与导出 | + +三项能力必须新建: + +1. **站内消息中心**:后端无 `oa_notice`,但前端顶栏铃铛与 `/user/notice` 已在调用 `/api/oa/notice/*`,属于前端等后端。 +2. **业务域**:学分、志愿、社团、场地、青马培养、专家评审均无表无接口。 +3. **OCR 与 PDF 生成**:无 OCR 依赖;PDF 只能通过服务器 LibreOffice 转换。 + +## 二、菜单规划 + +原则:不动现有 12 个一级菜单,不迁移已有子菜单(避免破坏角色授权)。新增 2 个一级菜单,其余挂到语义最近的现有菜单。 + +### 新增一级菜单 + +``` +成长中心 /growth menu_id 14500 +├ 我的成长 /growth/myHome 14501 +├ 学分明细 /growth/credit 14502 +├ 社会实践学分 /growth/practiceCredit 14503 +├ 学分预警 /growth/creditWarning 14504 +├ 学生画像 /growth/portrait 14505 +└ 推荐内容 /growth/recommend 14506 + +社团与志愿 /club menu_id 14520 +├ 志愿服务时长 /club/volunteerHour 14521 +├ 志愿时长审核 /club/volunteerReview 14522 +├ 志愿证明 /club/volunteerCertificate 14523 +├ 社团管理 /club/club 14524 +├ 社团活动 /club/activity 14525 +├ 活动审批 /club/activityReview 14526 +├ 场地预约 /club/venue 14527 +├ 场地审批 /club/venueReview 14528 +├ 社团年审 /club/annualReview 14529 +└ 社团评优 /club/award 14530 +``` + +### 挂到现有菜单 + +| 现有菜单 | 新增子项 | +| --- | --- | +| 数字化团员 14342 | 青马学员档案 `/gxmu/qmgcStudent`、培养任务 `/gxmu/qmgcTask`、考核评分 `/gxmu/qmgcScore`、主题团课库 `/gxmu/thematicCourse` | +| AI内容创作与宣传助手 14365 | AI辅助教学设计 `/gxmu/teachingDesign`、AI主题团日设计 `/gxmu/themeDayDesign` | +| 挑战杯 14372 | 专家评审 `/gxmu/expertReview`、专家库 `/gxmu/expertPool` | +| 系统设置 8574 | 消息模板 `/system/messageTemplate`、推送记录 `/system/messageLog` | + +`青马工程`(14353)保留为申报入口;新增学员档案管理培养期,两者不合并。 +消息中心的用户侧直接复用已有 `/user/notice` 与顶栏铃铛,前端零改动即可生效。 + +## 三、分期 + +### P0(可上线的最小闭环,约 53 人日) + +| # | 功能 | 交付物 | +| --- | --- | --- | +| 1 | 站内消息中心 | `oa_notice` + `oa_notice_record` + `/api/oa/notice/*` + 审核钩子 | +| 2 | 第二课堂学分 | 学分规则/记录表 + Excel 导入 + 明细页 + 汇总 | +| 3 | 学生成长主页 | 聚合接口 + 简历式页面 | +| 4 | 志愿服务 | 个人录入 + 桂志愿导入 + 二级学院审核 + 时长汇总 | +| 5 | 社团管理 | 社团主档 + 成员 + 指导老师 | +| 6 | 场地预约 | 场地主档 + 日历 + 冲突校验 + 审批 | +| 7 | 青马培养档案 | 学员主档 + 培养材料 + 任务清单 | +| 8 | 主题团课库 | 表 + 列表 + 爬虫入库 | +| 9 | 工程配套 | 菜单/按钮/角色 SQL + 字典 + 权限码 | + +### P1(第二批,约 63 人日) + +社会实践学分申报(四级审核 + 教务处模板导出)、学分预警、智能推荐、学生画像、社团年审、社团评优、年度志愿证明、社团年度报告、专家评审、AI 辅助教学设计与主题团日设计、审核流并行审批改造。 + +### P2(探索期,约 23 人日) + +AI 学员学习情况分析、AI 定制培养建议、OCR 公章识别、AI 数字人、移动端接口整理。 + +## 四、数据模型 + +统一规范:`id BIGINT AUTO_INCREMENT`、`tenant_id`、`user_id`、`deleted`、`create_time`、`update_time`。 +附件:单文件用 `file_id + file_url`;多文件用 JSON 数组字符串。 +审批统一复用 `gxmu_review_list(module, pk)`,模块名即业务表名。 + +建表脚本见: + +- `docs/sql/phase2_01_growth_credit.sql`:学分、社会实践、毕业要求、预警、画像、推荐 +- `docs/sql/phase2_02_volunteer.sql`:志愿服务与证明 +- `docs/sql/phase2_03_club_venue.sql`:社团、成员、活动、年审、评优、场地、预约 +- `docs/sql/phase2_04_qmgc.sql`:青马学员、材料、任务、评分、考试 +- `docs/sql/phase2_05_thematic_course.sql`:主题团课库 +- `docs/sql/phase2_06_notice.sql`:消息中心 +- `docs/sql/phase2_07_review_parallel.sql`:审核流并行节点改造 +- `docs/sql/phase2_08_menu_permission.sql`:菜单、按钮、角色授权 + +## 五、后端设计 + +新增包(不改动现有 24 个 gxmu 控制器): + +``` +com.gxwebsoft.gxmu.credit 学分规则、记录、导入、汇总、预警 +com.gxwebsoft.gxmu.volunteer 志愿时长与证明 +com.gxwebsoft.gxmu.club 社团、活动、年审、评优 +com.gxwebsoft.gxmu.venue 场地与预约 +com.gxwebsoft.gxmu.qmgcpro 青马培养档案(与现有 QmgcForm 申报区分) +com.gxwebsoft.gxmu.thematic 主题团课库 +com.gxwebsoft.gxmu.growth 成长主页聚合 +com.gxwebsoft.notice 消息中心 +``` + +接口约定: + +- 管理端:`/api/gxmu/{domain}/page|list|{id}|save|update|remove|batch`,沿用 `BaseController` + `BaseParam`,前端继续传 `backendAccess=true`。 +- 学生自助:`/api/gxmu/{domain}/my/*`,用 `hasOnlyStudentRole()` 收口,只返回本人数据。 +- 审核:不重复造轮子,统一写 `gxmu_review_list`,前端复用 `reviewStatusTag` / `reviewHistoryModal` / `reviewFlowModal`。 +- 消息中心按前端既有路径 `/api/oa/notice/*` 实现,顶栏铃铛与 `/user/notice` 无需改代码。 + +## 六、前端设计 + +新增目录: + +``` +src/views/growth/{myHome,credit,practiceCredit,creditWarning,portrait,recommend} +src/views/club/{volunteerHour,volunteerReview,volunteerCertificate,club,activity,activityReview,venue,venueReview,annualReview,award} +src/views/gxmu/{qmgcStudent,qmgcTask,qmgcScore,thematicCourse,teachingDesign,themeDayDesign,expertReview,expertPool} +src/views/system/{messageTemplate,messageLog} +``` + +复用清单: + +- 列表骨架 `ele-pro-table`,照 `views/gxmu/studentRoster`。 +- 导入 `views/gxmu/components/common-import.vue`。 +- 附件 `components/attachmentList.vue` + `views/gxmu/utils/attachment.ts`。 +- 审核 `reviewStatusTag` / `reviewHistoryModal` / `reviewFlowModal`。 +- 图表 `utils/use-echarts.ts`,照 `views/gxmu/youthDataCenter`。 +- AI 页复制 `activityOutlineGen` 壳 + `utils/ai-stream.ts`。 + +新建公共件: + +1. `components/MultiFileUpload`:统一多附件上传。 +2. `components/AiGenPageShell`:AI 生成页外壳。 +3. `utils/export.ts`:收敛 Excel/CSV/打印 PDF。 + +顺带修复: + +- `views/user/profile` 保存改用 `PUT /system/user/self`(学生账号当前会因缺 `sys:user:update` 失败)。 +- 统一「最新审核记录」排序规则到 `views/gxmu/utils/review.ts`。 + +## 七、实施顺序 + +1. 基建:消息中心 + 菜单权限 SQL + 公共上传组件。 +2. 学分域:规则/记录/导入 → 成长主页聚合 → 明细页。 +3. 志愿域:录入/导入 → 审核 → 汇总。 +4. 社团域:社团主档/成员 → 活动 → 场地 → 预约。 +5. 青马域:学员档案 → 任务 → 材料。 +6. 团课库:建表 → 爬虫 → 列表。 +7. P1 / P2 按上表推进。 + +## 八、待确认事项(有默认值,不阻塞 P0) + +| 事项 | 默认假设 | +| --- | --- | +| 第二课堂 / 桂志愿 是否有 API | 先 Excel 导入,`source/external_id` 预留 | +| 学分规则与毕业要求 | 按「学院 + 年级 + 专业 + 类别」配置应修学分 | +| 青马评分标准 | 做成可配置评分项与权重 | +| 社团年审量化指标 | 指标配置表,活动数/参与人次/稿件数自动取数 | +| 指导老师人数 | 社团主档存 1 名主指导,审批节点支持多人 | +| 志愿证明模板与电子章 | 需提供模板与印章图;未提供时用占位版 | +| 团委科研课题是否新申报模块 | 视为新模块,新增 `gxmu_research_project` | +| 数字人形象 | 先用团委现有卡通形象,不做 3D | diff --git a/src/api/growth/club/index.ts b/src/api/growth/club/index.ts new file mode 100644 index 0000000..4214c57 --- /dev/null +++ b/src/api/growth/club/index.ts @@ -0,0 +1,392 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 社团 */ +export interface Club { + id?: number; + clubCode?: string; + clubName?: string; + category?: string; + collegeId?: number; + organizationId?: number; + advisorUserId?: number; + advisorName?: string; + presidentUserId?: number; + presidentName?: string; + presidentPhone?: string; + memberCount?: number; + level?: string; + foundedDate?: string; + intro?: string; + cover?: string; + status?: number; + sortNumber?: number; + collegeName?: string; + members?: ClubMember[]; +} + +/** 社团成员 */ +export interface ClubMember { + id?: number; + clubId?: number; + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + roleName?: string; + joinTime?: string; + quitTime?: string; + status?: number; +} + +/** 社团活动 */ +export interface ClubActivity { + id?: number; + clubId?: number; + clubName?: string; + title?: string; + category?: string; + startTime?: string; + endTime?: string; + place?: string; + scale?: number; + budget?: number; + content?: string; + attachments?: string; + ocrStatus?: number; + ocrOrg?: string; + status?: number; + userId?: number; + createTime?: string; +} + +export interface ClubParam extends PageParam { + clubName?: string; + category?: string; + collegeId?: number; + advisorUserId?: number; + status?: number; +} + +export interface ClubActivityParam extends PageParam { + clubId?: number; + title?: string; + category?: string; + status?: number; + userId?: number; + startTimeStart?: string; + startTimeEnd?: string; +} + +/** 场地 */ +export interface Venue { + id?: number; + venueName?: string; + building?: string; + roomNo?: string; + capacity?: number; + facilities?: string; + openStartTime?: string; + openEndTime?: string; + openWeekdays?: string; + managerUserId?: number; + managerName?: string; + adminOrgId?: number; + status?: number; + remark?: string; + cover?: string; + sortNumber?: number; +} + +/** 场地预约 */ +export interface VenueBooking { + id?: number; + venueId?: number; + venueName?: string; + userId?: number; + applicantName?: string; + applicantPhone?: string; + clubId?: number; + clubName?: string; + bookingDate?: string; + startTime?: string; + endTime?: string; + purpose?: string; + attendeeCount?: number; + status?: number; + reviewRemark?: string; + createTime?: string; +} + +export interface VenueBookingParam extends PageParam { + venueId?: number; + userId?: number; + clubId?: number; + status?: number; + bookingDate?: string; + bookingDateStart?: string; + bookingDateEnd?: string; +} + +// ---------------- 社团 ---------------- + +export async function pageClubs(params: ClubParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/club/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listClubs(params?: ClubParam) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/club', + { params } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getClub(id: number) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/club/' + id + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addClub(data: Club) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/club', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateClub(data: Club) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/club', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeClub(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/club/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listClubMembers(clubId: number, status?: number) { + const res = await request.get>( + MODULES_API_URL + `/gxmu/club/${clubId}/member`, + { params: { status } } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addClubMember(data: ClubMember) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/club/member', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeClubMember(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/club/member/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function pageClubActivities(params: ClubActivityParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/club/activity/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function submitClubActivity(data: ClubActivity) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/club/activity', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function reviewClubActivity(data: ClubActivity) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/club/activity/review', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeClubActivity(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/club/activity/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listMyClubs() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/club/my' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +// ---------------- 场地 ---------------- + +export async function pageVenues(params: { page?: number; limit?: number; venueName?: string; building?: string; status?: number }) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/venue/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listVenues() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/venue' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addVenue(data: Venue) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/venue', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateVenue(data: Venue) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/venue', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeVenue(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/venue/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function pageVenueBookings(params: VenueBookingParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/venue/booking/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function createVenueBooking(data: VenueBooking) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/venue/booking', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function reviewVenueBooking(data: VenueBooking) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/venue/booking/review', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function cancelVenueBooking(id: number) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/venue/booking/cancel/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getVenueSchedule(venueId: number, date: string) { + const res = await request.get[]>>( + MODULES_API_URL + '/gxmu/venue/booking/schedule', + { params: { venueId, date } } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/credit/index.ts b/src/api/growth/credit/index.ts new file mode 100644 index 0000000..5a7678c --- /dev/null +++ b/src/api/growth/credit/index.ts @@ -0,0 +1,224 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 学分分类汇总 */ +export interface CategorySummary { + category: string; + earned: number; + required: number; + gap: number; + rate: number; + count: number; +} + +/** 学分汇总 */ +export interface CreditSummary { + userId: number; + studentName?: string; + studentNo?: string; + totalEarned: number; + totalRequired: number; + totalGap: number; + completionRate: number; + categories: CategorySummary[]; +} + +/** 学分记录 */ +export interface CreditRecord { + id?: number; + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + category?: string; + itemName?: string; + score?: number; + acquireTime?: string; + source?: string; + externalId?: string; + evidence?: string; + status?: number; + remark?: string; + createTime?: string; + updateTime?: string; +} + +export interface CreditRecordParam extends PageParam { + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + category?: string; + source?: string; + status?: number; + acquireTimeStart?: string; + acquireTimeEnd?: string; + backendAccess?: boolean; +} + +/** 学分规则 */ +export interface CreditRule { + id?: number; + category?: string; + itemName?: string; + score?: number; + source?: string; + gradeScope?: string; + collegeScope?: string; + status?: number; + remark?: string; +} + +/** 毕业学分要求 */ +export interface GraduateRequirement { + id?: number; + year?: number; + collegeId?: number; + major?: string; + category?: string; + requiredScore?: number; + sortNumber?: number; + remark?: string; +} + +/** 分页查询学分记录 */ +export async function pageCreditRecords(params: CreditRecordParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/credit/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 新增学分记录 */ +export async function addCreditRecord(data: CreditRecord) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 修改学分记录 */ +export async function updateCreditRecord(data: CreditRecord) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/credit', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 删除学分记录 */ +export async function removeCreditRecord(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/credit/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 导入一条学分记录 */ +export async function importCreditRecord(data: CreditRecord) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit/import', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 我的学分汇总 */ +export async function getMyCreditSummary() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/credit/my/summary' + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 我的应修学分要求 */ +export async function getMyCreditRequirements() { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/credit/my/requirements' + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 学分规则列表 */ +export async function listCreditRules() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/credit/rule' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 毕业要求列表 */ +export async function listGraduateRequirements() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/credit/requirement' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 新增毕业要求 */ +export async function addGraduateRequirement(data: GraduateRequirement) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit/requirement', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 修改毕业要求 */ +export async function updateGraduateRequirement(data: GraduateRequirement) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/credit/requirement', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 删除毕业要求 */ +export async function removeGraduateRequirement(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/credit/requirement/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/home/index.ts b/src/api/growth/home/index.ts new file mode 100644 index 0000000..bf4e048 --- /dev/null +++ b/src/api/growth/home/index.ts @@ -0,0 +1,65 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 学生成长主页 */ +export interface GrowthHome { + basic?: { + userId?: number; + name?: string; + studentNo?: string; + avatar?: string; + gender?: string; + collegeName?: string; + className?: string; + major?: string; + grade?: string; + phone?: string; + leaguePosition?: string; + joinMonth?: string; + politics?: string; + profileCompletion?: number; + }; + credit?: { + totalEarned?: number; + totalRequired?: number; + totalGap?: number; + completionRate?: number; + categories?: { + category: string; + earned: number; + required: number; + gap: number; + rate: number; + count: number; + }[]; + }; + volunteer?: Record; + activity?: Record; + declarations?: Record[]; + qmgc?: Record | null; +} + +/** 我的成长主页 */ +export async function getMyGrowthHome() { + const res = await request.get>( + MODULES_API_URL + '/gxmu/growth/my' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 指定学生的成长主页 */ +export async function getGrowthHome(userId: number) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/growth/home', + { params: { userId } } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/practice/index.ts b/src/api/growth/practice/index.ts new file mode 100644 index 0000000..6676f62 --- /dev/null +++ b/src/api/growth/practice/index.ts @@ -0,0 +1,132 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 社会实践学分申报 */ +export interface PracticeCreditApply { + id?: number; + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + schoolYear?: string; + activityName?: string; + activityType?: string; + startDate?: string; + endDate?: string; + roleName?: string; + content?: string; + score?: number; + approvedScore?: number; + attachments?: string; + status?: number; + remark?: string; + createTime?: string; + updateTime?: string; +} + +export interface PracticeCreditParam extends PageParam { + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + schoolYear?: string; + activityType?: string; + status?: number; + backendAccess?: boolean; +} + +export const PRACTICE_STATUS_OPTIONS = [ + { label: '草稿', value: 0 }, + { label: '待审核', value: 1 }, + { label: '通过', value: 2 }, + { label: '驳回', value: 3 }, + { label: '作废', value: 4 } +]; + +export const PRACTICE_TYPE_OPTIONS = [ + { label: '寒暑假社会实践', value: '寒暑假社会实践' }, + { label: '志愿服务', value: '志愿服务' }, + { label: '社会调研', value: '社会调研' }, + { label: '实习实训', value: '实习实训' }, + { label: '其他', value: '其他' } +]; + +export async function pagePracticeCredits(params: PracticeCreditParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/practice-credit/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function submitPracticeCredit(data: PracticeCreditApply) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/practice-credit/my', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function withdrawPracticeCredit(id: number) { + const res = await request.put>( + MODULES_API_URL + `/gxmu/practice-credit/${id}/withdraw` + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function voidPracticeCredit(id: number, reason?: string) { + const res = await request.put>( + MODULES_API_URL + `/gxmu/practice-credit/${id}/void`, + null, + { params: { reason } } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removePracticeCredit(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/practice-credit/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getMyPracticeSummary() { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/practice-credit/my/summary' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 导出教务处模板数据表 */ +export async function exportPracticeCredit(params: PracticeCreditParam) { + const res = await request.post>>( + MODULES_API_URL + '/gxmu/practice-credit/export', + params + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/qmgc/index.ts b/src/api/growth/qmgc/index.ts new file mode 100644 index 0000000..24a6804 --- /dev/null +++ b/src/api/growth/qmgc/index.ts @@ -0,0 +1,289 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 青马学员 */ +export interface QmgcStudent { + id?: number; + userId?: number; + formId?: number; + studentNo?: string; + studentName?: string; + gender?: string; + phone?: string; + collegeId?: number; + collegeName?: string; + classId?: number; + className?: string; + batch?: string; + year?: number; + candidateNo?: string; + stage?: string; + stageName?: string; + result?: string; + totalScore?: number; + remark?: string; + status?: number; + createTime?: string; +} + +/** 青马培养材料 */ +export interface QmgcMaterial { + id?: number; + studentId?: number; + stage?: string; + type?: string; + title?: string; + content?: string; + fileId?: number; + fileUrl?: string; + submitTime?: string; + score?: number; + scorerId?: number; + scoreComment?: string; + status?: number; +} + +/** 青马培养任务 */ +export interface QmgcTask { + id?: number; + batch?: string; + stage?: string; + title?: string; + type?: string; + linkPath?: string; + content?: string; + mustDo?: number; + score?: number; + deadline?: string; + sortNumber?: number; + status?: number; +} + +/** 青马考核评分 */ +export interface QmgcScore { + id?: number; + studentId?: number; + dimension?: string; + item?: string; + score?: number; + maxScore?: number; + scorerId?: number; + scorerName?: string; + comment?: string; + scoredTime?: string; +} + +export interface QmgcStudentParam extends PageParam { + userId?: number; + formId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + batch?: string; + year?: number; + stage?: string; + status?: number; +} + +export interface QmgcTaskParam extends PageParam { + batch?: string; + stage?: string; + type?: string; + status?: number; +} + +export async function pageQmgcStudents(params: QmgcStudentParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/qmgc-student/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getQmgcStudentDetail(id: number) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/qmgc-student/' + id + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addQmgcStudent(data: QmgcStudent) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/qmgc-student', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateQmgcStudent(data: QmgcStudent) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/qmgc-student', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeQmgcStudent(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/qmgc-student/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getMyQmgc() { + const res = await request.get | null>>( + MODULES_API_URL + '/gxmu/qmgc-student/my' + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listQmgcMaterials(studentId: number) { + const res = await request.get>( + MODULES_API_URL + `/gxmu/qmgc-student/${studentId}/material` + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function submitQmgcMaterial(data: QmgcMaterial) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/qmgc-student/material', + data + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function scoreQmgcMaterial(id: number, score: number, comment?: string) { + const res = await request.post>( + MODULES_API_URL + `/gxmu/qmgc-student/material/${id}/score`, + null, + { params: { score, comment } } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listQmgcStudentTasks(studentId: number) { + const res = await request.get[]>>( + MODULES_API_URL + `/gxmu/qmgc-student/${studentId}/task` + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function completeQmgcTask(studentId: number, taskId: number, proofFiles?: string) { + const res = await request.post>( + MODULES_API_URL + `/gxmu/qmgc-student/${studentId}/task/${taskId}/complete`, + null, + { params: { proofFiles } } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function assignQmgcTasks(studentId: number, batch?: string) { + const res = await request.post>( + MODULES_API_URL + `/gxmu/qmgc-student/${studentId}/task/assign`, + null, + { params: { batch } } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function pageQmgcTasks(params: QmgcTaskParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/qmgc-student/task/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addQmgcTask(data: QmgcTask) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/qmgc-student/task', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateQmgcTask(data: QmgcTask) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/qmgc-student/task', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeQmgcTask(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/qmgc-student/task/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function listQmgcScores(studentId: number) { + const res = await request.get>( + MODULES_API_URL + `/gxmu/qmgc-student/${studentId}/score` + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function saveQmgcScore(data: QmgcScore) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/qmgc-student/score', + data + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/thematic/index.ts b/src/api/growth/thematic/index.ts new file mode 100644 index 0000000..70e3f8f --- /dev/null +++ b/src/api/growth/thematic/index.ts @@ -0,0 +1,102 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 主题团课资源 */ +export interface ThematicCourse { + id?: number; + title?: string; + type?: string; + sourceSite?: string; + sourceUrl?: string; + sourceKey?: string; + cover?: string; + summary?: string; + content?: string; + fileId?: number; + fileUrl?: string; + tags?: string; + heat?: number; + publishTime?: string; + status?: number; + lastSyncTime?: string; + createTime?: string; +} + +export interface ThematicCourseParam extends PageParam { + type?: string; + tags?: string; + status?: number; +} + +export const THEMATIC_TYPE_OPTIONS = [ + { label: '课件', value: 'courseware' }, + { label: '讲稿', value: 'script' }, + { label: '视频', value: 'video' }, + { label: '案例', value: 'case' }, + { label: '活动方案', value: 'activity' } +]; + +export async function pageThematicCourses(params: ThematicCourseParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/thematic-course/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function getThematicCourse(id: number) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/thematic-course/' + id + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addThematicCourse(data: ThematicCourse) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/thematic-course', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateThematicCourse(data: ThematicCourse) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/thematic-course', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeThematicCourse(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/thematic-course/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function syncThematicCourses() { + const res = await request.post>( + MODULES_API_URL + '/gxmu/thematic-course/sync' + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/volunteer/index.ts b/src/api/growth/volunteer/index.ts new file mode 100644 index 0000000..270adb8 --- /dev/null +++ b/src/api/growth/volunteer/index.ts @@ -0,0 +1,136 @@ +import type { PageParam, PageResult } from '@/api'; +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 志愿服务时长记录 */ +export interface VolunteerRecord { + id?: number; + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + source?: string; + externalId?: string; + projectName?: string; + orgName?: string; + hours?: number; + serviceDate?: string; + serviceEndDate?: string; + content?: string; + proofFiles?: string; + status?: number; + reviewRemark?: string; + reviewerId?: number; + reviewTime?: string; + createTime?: string; + updateTime?: string; +} + +export interface VolunteerRecordParam extends PageParam { + userId?: number; + studentNo?: string; + studentName?: string; + collegeId?: number; + classId?: number; + source?: string; + status?: number; + serviceDateStart?: string; + serviceDateEnd?: string; + backendAccess?: boolean; +} + +/** 分页查询志愿时长 */ +export async function pageVolunteerRecords(params: VolunteerRecordParam) { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/volunteer/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 个人录入志愿时长 */ +export async function submitVolunteerRecord(data: VolunteerRecord) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/volunteer/my', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 导入志愿时长 */ +export async function importVolunteerRecord(data: VolunteerRecord) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/volunteer/import', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 审核志愿时长 */ +export async function reviewVolunteerRecord(data: VolunteerRecord) { + const res = await request.put>( + MODULES_API_URL + '/gxmu/volunteer/review', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 删除志愿时长 */ +export async function removeVolunteerRecord(id: number) { + const res = await request.delete>( + MODULES_API_URL + '/gxmu/volunteer/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 我的累计志愿时长 */ +export async function getMyVolunteerTotal(year?: number) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/volunteer/my/total', + { params: { year } } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 我的志愿时长年度汇总 */ +export async function getMyVolunteerSummary() { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/volunteer/my/summary' + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 我的志愿项目分组统计 */ +export async function getMyVolunteerProjectStats(year?: number) { + const res = await request.get[]>>( + MODULES_API_URL + '/gxmu/volunteer/my/project', + { params: { year } } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/growth/warning/index.ts b/src/api/growth/warning/index.ts new file mode 100644 index 0000000..956c0db --- /dev/null +++ b/src/api/growth/warning/index.ts @@ -0,0 +1,91 @@ +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { MODULES_API_URL } from '@/config/setting'; + +/** 学分类警 */ +export interface CreditWarning { + id?: number; + userId?: number; + category?: string; + requiredScore?: number; + earnedScore?: number; + gap?: number; + level?: string; + hitTime?: string; + notified?: number; + notifyMonth?: string; + studentName?: string; + studentNo?: string; +} + +export interface CreditWarningParam { + userId?: number; + category?: string; + level?: string; + notified?: number; + backendAccess?: boolean; +} + +export const WARNING_LEVEL_OPTIONS = [ + { label: '正常', value: 'normal' }, + { label: '注意', value: 'warn' }, + { label: '严重', value: 'danger' } +]; + +/** 查询预警列表 */ +export async function listCreditWarnings(params: CreditWarningParam) { + const res = await request.get>( + MODULES_API_URL + '/gxmu/credit-warning', + { params } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 预警汇总 */ +export async function getCreditWarningSummary() { + const res = await request.get>>( + MODULES_API_URL + '/gxmu/credit-warning/summary' + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 重算我的预警 */ +export async function recalcMyWarnings() { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit-warning/my/recalculate' + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 全量重算 */ +export async function recalcAllWarnings() { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit-warning/recalculate' + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 发送预警提醒 */ +export async function notifyCreditWarning(userId?: number) { + const res = await request.post>( + MODULES_API_URL + '/gxmu/credit-warning/notify', + null, + { params: { userId } } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/system/notice/index.ts b/src/api/system/notice/index.ts new file mode 100644 index 0000000..67eb71c --- /dev/null +++ b/src/api/system/notice/index.ts @@ -0,0 +1,95 @@ +import request from '@/utils/request'; +import type { ApiResult, PageResult } from '@/api'; + + +/** 消息模板 */ +export interface NoticeTemplate { + id?: number; + code?: string; + type?: string; + title?: string; + content?: string; + icon?: string; + color?: string; + status?: number; + createTime?: string; + updateTime?: string; +} + +/** 站内消息 */ +export interface NoticeItem { + noticeId?: number; + type?: string; + title?: string; + content?: string; + isRead?: number; + status?: number; + userId?: number; + senderName?: string; + createTime?: string; +} + +/** 分页查询消息模板(复用通用分页接口未提供时,前端直接读后端接口) */ +export async function pageNoticeTemplates(params: { + page?: number; + limit?: number; + keywords?: string; +}) { + const res = await request.get>>( + '/oa/notice-template/page', + { params } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function addNoticeTemplate(data: NoticeTemplate) { + const res = await request.post>( + '/oa/notice-template', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function updateNoticeTemplate(data: NoticeTemplate) { + const res = await request.put>( + '/oa/notice-template', + data + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +export async function removeNoticeTemplate(id: number) { + const res = await request.delete>( + '/oa/notice-template/' + id + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** 分页查询推送记录(后台视角,跨用户) */ +export async function pageNoticeLog(params: { + page?: number; + limit?: number; + type?: string; + keywords?: string; +}) { + const res = await request.get>>( + '/oa/notice/page', + { params: { ...params, backendAccess: true } } + ); + if (res.data.code === 0) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index 20cc70d..b336a2f 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -77,6 +77,20 @@ export async function addUser(data: User) { return Promise.reject(new Error(res.data.message)); } +/** + * 修改当前登录用户自己的资料。 + * + *

个人中心必须走这个接口:{@code PUT /system/user} 需要 sys:user:update 权限, + * 学生账号没有该权限,直接用会保存失败。 + */ +export async function updateSelfUser(data: User) { + const res = await request.put>("/system/user/self", data); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + /** * 修改用户 */ diff --git a/src/components/MultiFileUpload/index.vue b/src/components/MultiFileUpload/index.vue new file mode 100644 index 0000000..8089df9 --- /dev/null +++ b/src/components/MultiFileUpload/index.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/src/utils/ws-url.ts b/src/utils/ws-url.ts new file mode 100644 index 0000000..c117bff --- /dev/null +++ b/src/utils/ws-url.ts @@ -0,0 +1,30 @@ +import { API_BASE_URL } from '@/config/setting'; + +/** + * 统一拼接「当前登录用户」的 WebSocket 地址。 + * + *

2026-10-21 起统一走 {@code /api} 前缀(后端端点为 {@code /api/chat/{userId}})。 + * 原先这里会把 {@code /api} 主动剥掉再拼 {@code /chat/${userId}},现场网关 + * (twai-api.gxmu.edu.cn)只对 {@code /api} 放行 WebSocket 升级,导致握手被降级成 + * 普通 GET 而返回 404。现在与其它接口同前缀,减少一类环境差异。 + * + *

若后端回退到旧路径,只需把 {@link USER_WS_PATH} 改成 {@code '/chat'}。 + */ +const USER_WS_PATH = '/chat'; + +export const buildUserWebSocketUrl = (userId: number | string): string => { + const suffix = `${USER_WS_PATH}/${userId}`; + + // 绝对地址(生产/联调):https://host/api -> wss://host/api/chat/{userId} + if (/^https?:\/\//.test(API_BASE_URL)) { + const url = new URL(API_BASE_URL); + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.pathname = url.pathname.replace(/\/+$/, ''); + return `${url.toString().replace(/\/$/, '')}${suffix}`; + } + + // 相对地址(VITE_API_URL=/api):与页面同源 + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const base = API_BASE_URL.replace(/\/+$/, ''); + return `${protocol}//${window.location.host}${base}${suffix}`; +}; diff --git a/src/views/club/activity/components/edit.vue b/src/views/club/activity/components/edit.vue new file mode 100644 index 0000000..361b5bf --- /dev/null +++ b/src/views/club/activity/components/edit.vue @@ -0,0 +1,190 @@ + + + + diff --git a/src/views/club/activity/index.vue b/src/views/club/activity/index.vue new file mode 100644 index 0000000..48daa74 --- /dev/null +++ b/src/views/club/activity/index.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/views/club/activityReview/index.vue b/src/views/club/activityReview/index.vue new file mode 100644 index 0000000..d51599f --- /dev/null +++ b/src/views/club/activityReview/index.vue @@ -0,0 +1,202 @@ + + + diff --git a/src/views/club/annualReview/index.vue b/src/views/club/annualReview/index.vue new file mode 100644 index 0000000..57b70ad --- /dev/null +++ b/src/views/club/annualReview/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/club/award/index.vue b/src/views/club/award/index.vue new file mode 100644 index 0000000..b1f7f44 --- /dev/null +++ b/src/views/club/award/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/club/club/components/edit.vue b/src/views/club/club/components/edit.vue new file mode 100644 index 0000000..1ba2b06 --- /dev/null +++ b/src/views/club/club/components/edit.vue @@ -0,0 +1,171 @@ + + + + diff --git a/src/views/club/club/index.vue b/src/views/club/club/index.vue new file mode 100644 index 0000000..ceca673 --- /dev/null +++ b/src/views/club/club/index.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/src/views/club/venue/index.vue b/src/views/club/venue/index.vue new file mode 100644 index 0000000..51b5e35 --- /dev/null +++ b/src/views/club/venue/index.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/src/views/club/venueReview/index.vue b/src/views/club/venueReview/index.vue new file mode 100644 index 0000000..5c13c8e --- /dev/null +++ b/src/views/club/venueReview/index.vue @@ -0,0 +1,202 @@ + + + diff --git a/src/views/club/volunteerCertificate/index.vue b/src/views/club/volunteerCertificate/index.vue new file mode 100644 index 0000000..645a604 --- /dev/null +++ b/src/views/club/volunteerCertificate/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/src/views/club/volunteerHour/components/edit.vue b/src/views/club/volunteerHour/components/edit.vue new file mode 100644 index 0000000..29f06f3 --- /dev/null +++ b/src/views/club/volunteerHour/components/edit.vue @@ -0,0 +1,142 @@ + + + + diff --git a/src/views/club/volunteerHour/components/import.vue b/src/views/club/volunteerHour/components/import.vue new file mode 100644 index 0000000..47c3e08 --- /dev/null +++ b/src/views/club/volunteerHour/components/import.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/views/club/volunteerHour/index.vue b/src/views/club/volunteerHour/index.vue new file mode 100644 index 0000000..409414d --- /dev/null +++ b/src/views/club/volunteerHour/index.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/src/views/club/volunteerReview/index.vue b/src/views/club/volunteerReview/index.vue new file mode 100644 index 0000000..2ed2b0d --- /dev/null +++ b/src/views/club/volunteerReview/index.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/src/views/growth/credit/components/edit.vue b/src/views/growth/credit/components/edit.vue new file mode 100644 index 0000000..56e6ef1 --- /dev/null +++ b/src/views/growth/credit/components/edit.vue @@ -0,0 +1,148 @@ + + + + diff --git a/src/views/growth/credit/components/import.vue b/src/views/growth/credit/components/import.vue new file mode 100644 index 0000000..cea5fe6 --- /dev/null +++ b/src/views/growth/credit/components/import.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/views/growth/credit/index.vue b/src/views/growth/credit/index.vue new file mode 100644 index 0000000..daed7b6 --- /dev/null +++ b/src/views/growth/credit/index.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/src/views/growth/creditWarning/index.vue b/src/views/growth/creditWarning/index.vue new file mode 100644 index 0000000..df7494f --- /dev/null +++ b/src/views/growth/creditWarning/index.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/src/views/growth/myHome/index.vue b/src/views/growth/myHome/index.vue new file mode 100644 index 0000000..b48a3ad --- /dev/null +++ b/src/views/growth/myHome/index.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/src/views/growth/portrait/index.vue b/src/views/growth/portrait/index.vue new file mode 100644 index 0000000..b2b9b13 --- /dev/null +++ b/src/views/growth/portrait/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/growth/practiceCredit/components/edit.vue b/src/views/growth/practiceCredit/components/edit.vue new file mode 100644 index 0000000..70d0f24 --- /dev/null +++ b/src/views/growth/practiceCredit/components/edit.vue @@ -0,0 +1,155 @@ + + + + diff --git a/src/views/growth/practiceCredit/index.vue b/src/views/growth/practiceCredit/index.vue new file mode 100644 index 0000000..09ca721 --- /dev/null +++ b/src/views/growth/practiceCredit/index.vue @@ -0,0 +1,375 @@ + + + + + diff --git a/src/views/growth/recommend/index.vue b/src/views/growth/recommend/index.vue new file mode 100644 index 0000000..0fb6afe --- /dev/null +++ b/src/views/growth/recommend/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/gxmu/components/reviewStatusTag.vue b/src/views/gxmu/components/reviewStatusTag.vue index 68be6ec..08670e2 100644 --- a/src/views/gxmu/components/reviewStatusTag.vue +++ b/src/views/gxmu/components/reviewStatusTag.vue @@ -13,13 +13,12 @@ diff --git a/src/views/gxmu/expertPool/index.vue b/src/views/gxmu/expertPool/index.vue new file mode 100644 index 0000000..ff4ccee --- /dev/null +++ b/src/views/gxmu/expertPool/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/gxmu/expertReview/index.vue b/src/views/gxmu/expertReview/index.vue new file mode 100644 index 0000000..efbf0fa --- /dev/null +++ b/src/views/gxmu/expertReview/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/views/gxmu/qmgcScore/index.vue b/src/views/gxmu/qmgcScore/index.vue new file mode 100644 index 0000000..bbce961 --- /dev/null +++ b/src/views/gxmu/qmgcScore/index.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/src/views/gxmu/qmgcStudent/components/edit.vue b/src/views/gxmu/qmgcStudent/components/edit.vue new file mode 100644 index 0000000..a7cc11d --- /dev/null +++ b/src/views/gxmu/qmgcStudent/components/edit.vue @@ -0,0 +1,163 @@ + + + + diff --git a/src/views/gxmu/qmgcStudent/index.vue b/src/views/gxmu/qmgcStudent/index.vue new file mode 100644 index 0000000..ac33ed4 --- /dev/null +++ b/src/views/gxmu/qmgcStudent/index.vue @@ -0,0 +1,404 @@ + + + diff --git a/src/views/gxmu/qmgcTask/index.vue b/src/views/gxmu/qmgcTask/index.vue new file mode 100644 index 0000000..4a4bf81 --- /dev/null +++ b/src/views/gxmu/qmgcTask/index.vue @@ -0,0 +1,256 @@ + + + diff --git a/src/views/gxmu/teachingDesign/index.vue b/src/views/gxmu/teachingDesign/index.vue new file mode 100644 index 0000000..0134815 --- /dev/null +++ b/src/views/gxmu/teachingDesign/index.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/views/gxmu/thematicCourse/index.vue b/src/views/gxmu/thematicCourse/index.vue new file mode 100644 index 0000000..f0c210d --- /dev/null +++ b/src/views/gxmu/thematicCourse/index.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/views/gxmu/themeDayDesign/index.vue b/src/views/gxmu/themeDayDesign/index.vue new file mode 100644 index 0000000..709eb43 --- /dev/null +++ b/src/views/gxmu/themeDayDesign/index.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/views/gxmu/utils/review.ts b/src/views/gxmu/utils/review.ts index 97be334..1bd3be5 100644 --- a/src/views/gxmu/utils/review.ts +++ b/src/views/gxmu/utils/review.ts @@ -1,11 +1,24 @@ import type { ReviewList } from '@/api/gxmu/reviewList/model'; -export function shouldHideEditByReviewList(reviewList?: ReviewList[]) { +/** + * 取最新一条审核记录。 + * + *

统一排序规则:先按 sortNumber(并行组序号)降序,再按 createTime、id 降序。 + * 原先 reviewStatusTag 只按 id、这里只按 createTime+id,两处可能给出不同结论 + * (并行审核某组后审通过、前组已驳回时,状态标签与「能否编辑」会互相矛盾), + * 现在收敛到这一处。 + */ +export function getLatestReview(reviewList?: ReviewList[]): ReviewList | undefined { const list = reviewList ?? []; if (!list.length) { - return false; + return undefined; } - const latest = [...list].sort((a, b) => { + return [...list].sort((a, b) => { + const sortA = Number(a.sortNumber ?? 0); + const sortB = Number(b.sortNumber ?? 0); + if (sortA !== sortB) { + return sortB - sortA; + } const timeA = Date.parse(a.createTime || ''); const timeB = Date.parse(b.createTime || ''); const safeTimeA = Number.isFinite(timeA) ? timeA : 0; @@ -15,5 +28,15 @@ export function shouldHideEditByReviewList(reviewList?: ReviewList[]) { } return Number(b.id || 0) - Number(a.id || 0); })[0]; - return latest?.status !== 2; +} + +/** + * 最新记录不是驳回时,隐藏编辑入口(待审核或已通过都不允许改)。 + */ +export function shouldHideEditByReviewList(reviewList?: ReviewList[]) { + const list = reviewList ?? []; + if (!list.length) { + return false; + } + return getLatestReview(list)?.status !== 2; } diff --git a/src/views/system/messageLog/index.vue b/src/views/system/messageLog/index.vue new file mode 100644 index 0000000..9996633 --- /dev/null +++ b/src/views/system/messageLog/index.vue @@ -0,0 +1,131 @@ + + + diff --git a/src/views/system/messageTemplate/index.vue b/src/views/system/messageTemplate/index.vue new file mode 100644 index 0000000..5639e6c --- /dev/null +++ b/src/views/system/messageTemplate/index.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/views/user/profile/index.vue b/src/views/user/profile/index.vue index 67bda2f..200badb 100644 --- a/src/views/user/profile/index.vue +++ b/src/views/user/profile/index.vue @@ -263,7 +263,7 @@ import { storeToRefs } from 'pinia'; import { useThemeStore } from '@/store/modules/theme'; import type { User } from '@/api/system/user/model'; - import { updateUser } from '@/api/system/user'; + import { updateSelfUser } from '@/api/system/user'; import SexSelect from './components/sex-select.vue'; import { getMobile } from '@/utils/common'; import request from '@/utils/request'; @@ -378,10 +378,17 @@ form.province = city.value[0]; form.city = city.value[1]; form.region = city.value[2]; - updateUser(form).then((res) => { - loading.value = false; - message.success('保存成功'); - }); + // 个人中心走 /system/user/self:学生账号没有 sys:user:update 权限, + // 用管理员接口会直接 403 + updateSelfUser(form) + .then(() => { + loading.value = false; + message.success('保存成功'); + }) + .catch((e: any) => { + loading.value = false; + message.error(e?.message || '保存失败'); + }); }) .catch(() => {}); }; @@ -397,10 +404,17 @@ console.log(res.data.data.path); form.avatar = FILE_SERVER + res.data.data.path; visible.value = false; - updateUser(form).then((res) => { - loading.value = false; - message.success('保存成功'); - }); + // 个人中心走 /system/user/self:学生账号没有 sys:user:update 权限, + // 用管理员接口会直接 403 + updateSelfUser(form) + .then(() => { + loading.value = false; + message.success('保存成功'); + }) + .catch((e: any) => { + loading.value = false; + message.error(e?.message || '保存失败'); + }); }) .catch((e) => { console.error(e); @@ -412,7 +426,7 @@ console.log(result); form.avatar = result; visible.value = false; - updateUser(form).then((res) => { + updateSelfUser(form).then(() => { loading.value = false; message.success('保存成功'); });