线上问题的根因 - ClassIsland 插件默认每 15 秒上报一次,每次都带全量课表,上报完还顺手把课表镜像回教室 (本机 `UploadSchedule` / `MirrorSchedule` 两个开关默认都是开)。于是教室的课表每 15 秒被重写、 服务端也每 15 秒 merge + 清理一次幽灵行 —— 老师看到的就是"课表被反复删改"。 - 现在两个方向都只由 Web 端的一次性待办触发,插件侧那两个开关整体删除; 定时上报只报**运行状态**(当前上什么课),不带课表。 课表同步(#3) - 新增 `IntegrationDevice.requestMirrorSchedule` + `POST /integrations/devices/:id/mirror-schedule` (「下发课表」),与既有的 `request-schedule`(「获取课表」)对称;拉取成功即清标记 - 插件 `ClassPlanWriter.Apply(..., force: true)`:停用同一天同一单双周相位下冲突的旧课表 —— **只停用不删除**,内容原样留在档案里,老师随时能重新启用 - 插件设置页删掉「自动上报课表」「镜像回 ClassIsland」两个开关,改为一句说明 「客户端」页(#4) - 把「ClassIsland 联动」与「客户端设备」合成一页 `ClientHubView.vue`:一个班一张卡片, 两条链路状态放一起,勾选可批量下发课表 / 更新,点卡片进详情做逐台操作 - 「下发提醒」也搬了过来;入口权限用 `classisland:notify`(默认 ADMIN + TEACHER), 页内按钮再各自按 device:manage / remote:control / client:update-push 显示或禁用 - 两个旧页面 `meta.hidden: true` **保留路由**(安装包缓存管理、联动步骤说明仍在), 从详情抽屉里给链接 —— 不删功能 成绩(#5/#6/#7) - 新增 `Exam`(班级 + 考试名 + `countInTotal`)与迁移;`GET/PUT /api/grades/exams`、 `GET /api/grades/ranking`(两条都**不挂权限键**,班级端的有效权限是空集,边界在 service 里) - 排名口径写在服务端:**同一学生同一科目只取最高分**(补录 / 重考会多行,不去重等于"总分多一科")、 并列同名次(1、2、2、4) - 「成绩录入」页加「考试口径」开关(按班级 + 考试名记住);客户端「成绩」页新增总分榜 + 单科榜 - `updateStudentGradeQuery` 改成只有教师 / 管理员能改 —— 原先班级端自己也能改 - 客户端的成绩查询开关改为只读(只显示服务端下发的状态) 其他(#2/#5/#8/#9) - 全校学期开始日期改成 `AppSetting` 的 `term.startDate`,管理员在「学期周次」页直接改、立刻重算周次 (原先这一级只能来自 `.env`) - 大模型助手发紧急通知补上「上课时段 3 秒全屏警告」:判据抽成 `composables/useClassInSession.ts`, 与手动发布页共用 - 客户端去掉「关怀模式」「自动更新」两个开关(教室机器是统一投放的终端) - 修掉「选中设置二级菜单后收起分组、高亮块仍留在原地」:底片只在路由 / 滚动 / 缩放时重测, 而收起分组动不了 `.aside` 的盒子 —— 补上"子项被 overflow:hidden 裁掉"的判据 + 观察分组 `<ul>` 高度 - `src/types/express.d.ts` 里 `req.device` 改成引用 `DeviceContext`:原先手抄了一份结构, 往接口加字段时两边就漂了,而报错会落在 `getDevice()` 的 `return device;` 上,看不出真正原因 验收:`lint` / `typecheck` / `check:icons` / `docs:check` 全绿; 后端端到端 **247/247**、联动链路 **44/44**、插件静态契约 **76/76**、 Web 真实点击 **34/34**(含新增的「客户端」页用例)、客户端冒烟 **90/90**。
97 lines
3.3 KiB
YAML
97 lines
3.3 KiB
YAML
# ClassHelper · 生产部署(MySQL + 服务端 + 可选 HTTPS 反代)
|
|
#
|
|
# 使用:
|
|
# 1) 复制环境变量模板并填写密钥:cp deploy/.env.example deploy/.env
|
|
# 2) 构建并启动:docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build
|
|
# 3) 访问:http://<服务器IP>:4000/
|
|
#
|
|
# 说明:数据库迁移由一次性的 migrate 服务执行(prisma migrate deploy),
|
|
# 服务端容器因此无需内置 Prisma CLI。
|
|
|
|
name: classhelper
|
|
|
|
services:
|
|
# ---------------------------------------------------------------- 数据库
|
|
db:
|
|
image: mysql:8.4
|
|
restart: unless-stopped
|
|
command:
|
|
- --character-set-server=utf8mb4
|
|
- --collation-server=utf8mb4_unicode_ci
|
|
- --default-time-zone=+08:00
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请在 deploy/.env 中设置}
|
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-classhelper}
|
|
MYSQL_USER: ${MYSQL_USER:-classhelper}
|
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?请在 deploy/.env 中设置}
|
|
volumes:
|
|
- db-data:/var/lib/mysql
|
|
healthcheck:
|
|
test: ['CMD', 'mysqladmin', 'ping', '-h', '127.0.0.1', '-p${MYSQL_ROOT_PASSWORD}']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 40s
|
|
networks: [classhelper]
|
|
|
|
# ---------------------------------------------------------------- 数据库迁移(一次性)
|
|
migrate:
|
|
build:
|
|
context: ..
|
|
dockerfile: deploy/Dockerfile
|
|
target: build
|
|
restart: 'no'
|
|
working_dir: /app/packages/server
|
|
environment:
|
|
DATABASE_PROVIDER: mysql
|
|
DATABASE_URL: mysql://${MYSQL_USER:-classhelper}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE:-classhelper}
|
|
# MySQL 需要 mariadb 驱动适配器(切换数据库时按 docs/management/mysql.md 安装)
|
|
NODE_ENV: production
|
|
command: >
|
|
sh -c "pnpm --filter @classhelper/server add @prisma/adapter-mariadb &&
|
|
npx prisma migrate deploy"
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
networks: [classhelper]
|
|
|
|
# ---------------------------------------------------------------- 服务端 + Web 管理端
|
|
server:
|
|
build:
|
|
context: ..
|
|
dockerfile: deploy/Dockerfile
|
|
target: runtime
|
|
image: classhelper-server:${APP_VERSION:-1.3.1}
|
|
restart: unless-stopped
|
|
environment:
|
|
NODE_ENV: production
|
|
HOST: 0.0.0.0
|
|
PORT: 4000
|
|
DATABASE_PROVIDER: mysql
|
|
DATABASE_URL: mysql://${MYSQL_USER:-classhelper}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE:-classhelper}
|
|
JWT_SECRET: ${JWT_SECRET:?请在 deploy/.env 中设置(48 字节随机串)}
|
|
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
|
|
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
|
TRUST_PROXY: '1'
|
|
RATE_LIMIT_ENABLED: 'true'
|
|
STARTUP_DB_CHECK: 'true'
|
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
TERM_START_DATE: ${TERM_START_DATE:-}
|
|
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
|
|
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-admin123}
|
|
|
|
DEFAULT_CLASS_PASSWORD: ${DEFAULT_CLASS_PASSWORD:-123456}
|
|
DEFAULT_TEACHER_PASSWORD: ${DEFAULT_TEACHER_PASSWORD:-123456}
|
|
ports:
|
|
- '${SERVER_PORT:-4000}:4000'
|
|
depends_on:
|
|
db: { condition: service_healthy }
|
|
migrate: { condition: service_completed_successfully }
|
|
networks: [classhelper]
|
|
|
|
networks:
|
|
classhelper:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
db-data:
|