@@ -0,0 +1,5 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Notice 生产配置(线上以 nacos 的 notice.yaml 为准,本文件是 nacos 不可达时的兜底)
|
||||||
|
# 真正上线请在 Nacos 新建 dataId=notice.yaml、group=NOTICE 的配置覆盖以下项
|
||||||
|
# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
|
||||||
|
server:
|
||||||
|
port: 8070
|
||||||
|
servlet:
|
||||||
|
# 对外经 caddy 暴露为 http://base.yunhaixingtong.com/acaclassroom
|
||||||
|
context-path: /acaclassroom
|
||||||
|
|
||||||
|
spring:
|
||||||
|
mvc:
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
jackson:
|
||||||
|
time-zone: GMT+8
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
datasource:
|
||||||
|
# 与 UserBase 共用同一台 mysql8;库名为 aca_classroom(需先在 mysql8 上建库)
|
||||||
|
url: jdbc:mysql://mysql8:3306/aca_classroom?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||||
|
username: root
|
||||||
|
password: 'CVf^x$sW*b6CGHxh'
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
min-idle: 10
|
||||||
|
max-active: 30
|
||||||
|
jpa:
|
||||||
|
database: MySQL
|
||||||
|
show-sql: false
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||||
|
properties:
|
||||||
|
hibernate.order_by.default_null_ordering: last
|
||||||
|
open-in-view: true
|
||||||
|
|
||||||
|
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
|
||||||
|
yh:
|
||||||
|
sso:
|
||||||
|
server-url-prefix: http://sso.yunhaixingtong.com
|
||||||
|
client-host-url: http://base.yunhaixingtong.com/acaclassroom
|
||||||
|
allow-headers: all
|
||||||
|
allow-origins: all
|
||||||
|
url-pattern: /api/*
|
||||||
|
app-auth:
|
||||||
|
enabled: true
|
||||||
|
# 本应用自身身份(客户端签名用)
|
||||||
|
app-key: 100617
|
||||||
|
app-secret: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
|
||||||
|
# 受信应用(服务端验签用):appKey -> appSecret
|
||||||
|
trusted-apps:
|
||||||
|
100617: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
|
||||||
|
|
||||||
|
# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data
|
||||||
|
FILE_PATH: /data/upload
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Notice 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml)
|
||||||
|
# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到
|
||||||
|
# 镜像内烤好的 application-prod.yml 兜底默认。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[run-app] starting Notice (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..."
|
||||||
|
exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/acaclassroom.jar \
|
||||||
|
--spring.profiles.active=prod
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>2.7.18</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.yunhai</groupId>
|
||||||
|
<artifactId>acaclassroom</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<name>AcaClassroom</name>
|
||||||
|
<description>教室管理子应用</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>1.8</java.version>
|
||||||
|
<!-- 源码统一 UTF-8:命令行 mvn 编译在中文 Windows 上缺省走 GBK,中文/特殊字符字面量会乱码 -->
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<!-- 基础平台版本管理(含 yh-* 架包与 spring-boot-dependencies 对齐) -->
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunhai.platform</groupId>
|
||||||
|
<artifactId>yh-platform-dependencies</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Spring Cloud Alibaba(Nacos 配置中心,对齐 Boot 2.7) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||||
|
<version>2021.0.5.0</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 仅引入 Spring Security 的加密模块(BCryptPasswordEncoder),不引入整套安全框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-crypto</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Nacos 配置中心(替代 Spring Cloud Config)。Spring Cloud Alibaba 2021.0.5 适配 Spring Boot 2.7 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Boot 2.4+ 默认不再处理 bootstrap.yml,需显式引入以继续从 Nacos 拉取配置 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||||
|
<version>3.1.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON 序列化(fastjson,主类开启安全模式) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<version>1.2.80</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Swagger / OpenAPI 文档(springdoc,兼容 Boot 2.7) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-ui</artifactId>
|
||||||
|
<version>1.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 基础平台全家桶(聚合 starter):认证(CAS/会话/SSO)+ 应用间签名鉴权 + 用户中心静态门面
|
||||||
|
+ 统一响应 + actuator 健康探活,一行带齐(模块构成见 BasePlatform/后端项目知识库.md)。
|
||||||
|
约定:标准业务服务一律引聚合 starter;平台能力提供方(如 UserBase)按需单引。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunhai.platform</groupId>
|
||||||
|
<artifactId>yh-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- CAS 客户端(浏览器 SSO 重定向 / ticket 校验 / getRemoteUser 包装)。
|
||||||
|
版本 3.5.1 已在本地 .m2 仓库。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jasig.cas.client</groupId>
|
||||||
|
<artifactId>cas-client-core</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Excel 导入导出(EasyExcel 3.x,注解驱动、流式写入内存友好) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>easyexcel</artifactId>
|
||||||
|
<version>3.3.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 不引 spring-boot-starter-data-redis / cache:代码零使用,data-redis 会自动装配
|
||||||
|
RedisHealthIndicator,连不上 redis 会把 /actuator/health 整体拉成 DOWN(503)。 -->
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>acaclassroom</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<!-- 关闭增量编译,避免 Windows 下 maven-status 状态文件被锁导致
|
||||||
|
"Error while storing the mojo status / 拒绝访问" -->
|
||||||
|
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<!-- 打可执行 fat jar(Docker 镜像直接 java -jar 启动) -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.yunhai.acaclassroom.AcaClassroomApplication</mainClass>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>nexus-public</id>
|
||||||
|
<name>bsch/nexus public</name>
|
||||||
|
<url>http://nexus.yunhaixingtong.com/repository/maven-public/</url>
|
||||||
|
<releases><enabled>true</enabled></releases>
|
||||||
|
<!-- 快照 updatePolicy=never:平台 yh-* 是 SNAPSHOT,其 YschBaseOpenApi 曾在
|
||||||
|
openapi.client↔openapi.base 间漂移;钉 never 让本地沿用已缓存构建,避免无 -U 时被静默刷新而断链。
|
||||||
|
需要跟随平台更新时,手动 `mvn -U`(会覆盖本策略)或清 ~/.m2 后重建。 -->
|
||||||
|
<snapshots><enabled>true</enabled><updatePolicy>never</updatePolicy></snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<!--
|
||||||
|
ci 构建档案:将构建产物重定向到 IDE(IntelliJ/Eclipse)索引目录之外,
|
||||||
|
避免 IDE 后台进程持续锁定 target/maven-status/.../createdFiles.lst 导致
|
||||||
|
"Error while storing the mojo status / 拒绝访问"。
|
||||||
|
输出目录需从未被 IDE 打开过。常规本地构建无需激活此 profile。
|
||||||
|
-->
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>ci</id>
|
||||||
|
<build>
|
||||||
|
<directory>C:/aca-classroom-build/acaclassroom</directory>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
</project>
|
||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 教室管理后端冒烟脚本(Node 18+,无第三方依赖)。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node smoke/smoke.mjs # 无认证模式(本地 YH_SSO_URLPATTERN=/none/* + ACACLASSROOM_DEVCONTEXT_ENABLED=true)
|
||||||
|
* TEACHER_COOKIE="CASTGC=..." node smoke/smoke.mjs # 真实 SSO 会话(浏览器复制 Cookie)
|
||||||
|
*
|
||||||
|
* 覆盖:健康检查 → 基础/校区/类型/楼栋/教室 全链路(自建数据 → 断言 → 逆序清理)。
|
||||||
|
*/
|
||||||
|
const BASE = process.env.SMOKE_BASE || "http://localhost:8106";
|
||||||
|
const COOKIE = process.env.TEACHER_COOKIE || "";
|
||||||
|
const headers = { ...(COOKIE ? { Cookie: COOKIE } : {}) };
|
||||||
|
|
||||||
|
let passed = 0, failed = 0;
|
||||||
|
const ok = (name, cond, detail = "") => {
|
||||||
|
if (cond) { passed++; console.log(` ✓ ${name}`); }
|
||||||
|
else { failed++; console.error(` ✗ ${name} ${detail}`); }
|
||||||
|
};
|
||||||
|
|
||||||
|
async function api(method, path, { form, json, params } = {}) {
|
||||||
|
const url = new URL(BASE + path);
|
||||||
|
if (params) Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
|
||||||
|
const opt = { method, headers: { ...headers } };
|
||||||
|
if (json != null) {
|
||||||
|
opt.headers["Content-Type"] = "application/json;charset=UTF-8";
|
||||||
|
opt.body = JSON.stringify(json);
|
||||||
|
} else if (form != null) {
|
||||||
|
opt.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
||||||
|
opt.body = new URLSearchParams(form).toString();
|
||||||
|
}
|
||||||
|
const res = await fetch(url, opt);
|
||||||
|
const text = await res.text();
|
||||||
|
try { return JSON.parse(text); } catch { return { code: -1, msg: text.substring(0, 120) }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[smoke] target=${BASE} auth=${COOKIE ? "cookie" : "none(dev-context)"}`);
|
||||||
|
|
||||||
|
// 1. 健康
|
||||||
|
const health = await fetch(`${BASE}/actuator/health`).then(r => r.json()).catch(() => null);
|
||||||
|
ok("actuator/health UP", health && health.status === "UP");
|
||||||
|
|
||||||
|
// 2. 基础
|
||||||
|
const info = await api("GET", "/api/base/GetUserInfo");
|
||||||
|
ok("GetUserInfo code=0", info.code === 0, JSON.stringify(info).substring(0, 80));
|
||||||
|
const mods = await api("GET", "/api/base/GetUserMod");
|
||||||
|
ok("GetUserMod code=0", mods.code === 0);
|
||||||
|
|
||||||
|
// 3. 校区
|
||||||
|
const stamp = Date.now().toString(36);
|
||||||
|
const campusName = `冒烟校区${stamp}`;
|
||||||
|
const cAdd = await api("POST", "/api/campus/Add", { form: { campusName, sortOrder: 99 } });
|
||||||
|
ok("campus Add", cAdd.code === 0 && cAdd.data > 0, cAdd.msg);
|
||||||
|
const campusId = cAdd.data;
|
||||||
|
const cDup = await api("POST", "/api/campus/Add", { form: { campusName } });
|
||||||
|
ok("campus Add 重名拒绝", cDup.code !== 0);
|
||||||
|
const cList = await api("GET", "/api/campus/List");
|
||||||
|
ok("campus List 含新校区", cList.code === 0 && cList.data.some(c => c.serID === campusId));
|
||||||
|
|
||||||
|
// 4. 教室类型
|
||||||
|
const typeName = `冒烟类型${stamp}`;
|
||||||
|
const tAdd = await api("POST", "/api/classtype/Add", { form: { typeName } });
|
||||||
|
ok("classtype Add", tAdd.code === 0, tAdd.msg);
|
||||||
|
const typeId = tAdd.data;
|
||||||
|
|
||||||
|
// 5. 楼栋
|
||||||
|
const bAdd = await api("POST", "/api/building/Add", { form: { campusId, buildingName: `冒烟楼栋${stamp}`, buildingCode: `S${stamp.slice(-3)}`, floorCount: 2 } });
|
||||||
|
ok("building Add", bAdd.code === 0, bAdd.msg);
|
||||||
|
const buildingId = bAdd.data;
|
||||||
|
|
||||||
|
// 6. 教室批量添加(带设备)+ 列表
|
||||||
|
const eqSeed = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
|
||||||
|
ok("equipment Add(独立台账)", eqSeed.code === 0, eqSeed.msg);
|
||||||
|
const eqSeedId = eqSeed.data;
|
||||||
|
const eqSeedDup = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
|
||||||
|
ok("equipment Add 校内重名拒绝", eqSeedDup.code !== 0);
|
||||||
|
|
||||||
|
const batch = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 2, countPerFloor: 3, typeId, equipments: [{ equipmentId: eqSeedId, quantity: 2 }] } });
|
||||||
|
ok("classroom BatchAdd 6 间(带设备,每间数量 2)", batch.code === 0 && batch.data.created === 6, batch.msg);
|
||||||
|
const batchBad = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 9, countPerFloor: 1, typeId } });
|
||||||
|
ok("BatchAdd 楼层超限拒绝", batchBad.code !== 0);
|
||||||
|
const rooms = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||||||
|
ok("classroom List 6 间", rooms.code === 0 && rooms.data.total === 6, `total=${rooms.data && rooms.data.total}`);
|
||||||
|
const room1 = rooms.data.list[0].serID;
|
||||||
|
ok("教室行含设备关联(数量 2,名称×2)", rooms.data.list.every(r => Array.isArray(r.equipmentIds) && r.equipmentIds.includes(eqSeedId) && r.equipmentQtys && r.equipmentQtys[eqSeedId] === 2 && /冒烟设备.*×2$/.test(r.equipmentNames)), JSON.stringify(rooms.data.list[0] && rooms.data.list[0].equipmentNames));
|
||||||
|
const floors = await api("GET", "/api/classroom/Floors", { params: { buildingId } });
|
||||||
|
ok("classroom Floors [1,2]", floors.code === 0 && JSON.stringify(floors.data) === "[1,2]");
|
||||||
|
const bStats = await api("GET", "/api/building/Stats", { params: { campusId } });
|
||||||
|
ok("building Stats total=6", bStats.code === 0 && bStats.data.some(b => b.serID === buildingId && b.total === 6));
|
||||||
|
|
||||||
|
// 7. 教室编辑(含设备全量替换)+ 编号守卫
|
||||||
|
const edit = await api("POST", "/api/classroom/Edit", { json: { serID: room1, roomName: `冒烟教室${stamp}`, status: "closed", capacity: 50, seatCount: 45, equipmentIds: [] } });
|
||||||
|
ok("classroom Edit(清空设备)", edit.code === 0, edit.msg);
|
||||||
|
const roomsAfterEdit = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||||||
|
const room1After = roomsAfterEdit.data.list.find(r => r.serID === room1);
|
||||||
|
ok("Edit 后教室设备已清空", room1After && room1After.equipmentIds.length === 0);
|
||||||
|
const editQty = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipments: [{ equipmentId: eqSeedId, quantity: 3 }] } });
|
||||||
|
ok("classroom Edit equipments 按数量替换", editQty.code === 0, editQty.msg);
|
||||||
|
const roomsAfterQty = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||||||
|
const room1Qty = roomsAfterQty.data.list.find(r => r.serID === room1);
|
||||||
|
ok("Edit 后设备数量为 3(名称×3)", room1Qty && room1Qty.equipmentQtys && room1Qty.equipmentQtys[eqSeedId] === 3 && /冒烟设备.*×3$/.test(room1Qty.equipmentNames), JSON.stringify(room1Qty && room1Qty.equipmentQtys));
|
||||||
|
const editClear = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipmentIds: [] } });
|
||||||
|
ok("classroom Edit 旧字段 equipmentIds 清空设备(兼容)", editClear.code === 0, editClear.msg);
|
||||||
|
const room2code = roomsAfterEdit.data.list[1].roomCode;
|
||||||
|
const editDup = await api("POST", "/api/classroom/Edit", { json: { serID: roomsAfterEdit.data.list[1].serID, roomCode: roomsAfterEdit.data.list[2].roomCode } });
|
||||||
|
ok("Edit 楼栋内编号重复拒绝", editDup.code !== 0);
|
||||||
|
|
||||||
|
// 8. 设备编辑 + 引用删除守卫(设备被教室引用时不可删)
|
||||||
|
const eqList = await api("GET", "/api/equipment/List");
|
||||||
|
ok("equipment List 含引用数与统计数量(5 间 × 每间 2 = 10)", eqList.code === 0 && eqList.data.list.some(e => e.serID === eqSeedId && e.classroomCount === 5 && e.quantity === 10), JSON.stringify(eqList.data && eqList.data.list.find(e => e.serID === eqSeedId)));
|
||||||
|
const eqEdit = await api("POST", "/api/equipment/Edit", { form: { serID: eqSeedId, equipmentName: `冒烟设备${stamp}` } });
|
||||||
|
ok("equipment Edit", eqEdit.code === 0, eqEdit.msg);
|
||||||
|
const eqDelGuarded = await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } });
|
||||||
|
ok("equipment Delete 被引用守卫拒绝", eqDelGuarded.code !== 0);
|
||||||
|
|
||||||
|
// 9. 新增接口:列表排序 / 楼栋楼层分布 / 教室批量编辑
|
||||||
|
const sortedDesc = await api("GET", "/api/classroom/List", {
|
||||||
|
params: { buildingId, page: 1, count: 20, sortField: "capacity", order: "desc" },
|
||||||
|
});
|
||||||
|
ok(
|
||||||
|
"List 排序 capacity desc",
|
||||||
|
sortedDesc.code === 0 &&
|
||||||
|
sortedDesc.data.list[0].capacity >= sortedDesc.data.list[sortedDesc.data.list.length - 1].capacity,
|
||||||
|
JSON.stringify(sortedDesc.data && sortedDesc.data.list.map(r => r.capacity))
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedFloorDesc = await api("GET", "/api/classroom/List", {
|
||||||
|
params: { buildingId, page: 1, count: 20, sortField: "floor", order: "desc" },
|
||||||
|
});
|
||||||
|
ok("List 排序 floor desc 首行为 2 层", sortedFloorDesc.code === 0 && sortedFloorDesc.data.list[0].floor === 2);
|
||||||
|
|
||||||
|
const defaultOrder = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||||||
|
ok(
|
||||||
|
"List 不传排序参数保持默认序(楼层升序 + 编号升序)",
|
||||||
|
defaultOrder.code === 0 &&
|
||||||
|
defaultOrder.data.list[0].floor === 1 &&
|
||||||
|
defaultOrder.data.list[0].roomCode <= defaultOrder.data.list[1].roomCode
|
||||||
|
);
|
||||||
|
|
||||||
|
const bStatsFloors = await api("GET", "/api/building/Stats", { params: { campusId } });
|
||||||
|
const mineBuilding = bStatsFloors.data && bStatsFloors.data.find(b => b.serID === buildingId);
|
||||||
|
ok(
|
||||||
|
"building Stats 新增 floors 楼层分布(2 层,每层 3 间)",
|
||||||
|
mineBuilding &&
|
||||||
|
Array.isArray(mineBuilding.floors) &&
|
||||||
|
mineBuilding.floors.length === 2 &&
|
||||||
|
mineBuilding.floors[0].floor === 1 &&
|
||||||
|
mineBuilding.floors[0].total === 3 &&
|
||||||
|
mineBuilding.floors[1].floor === 2,
|
||||||
|
JSON.stringify(mineBuilding && mineBuilding.floors)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 9.5 空间作用域查询(教室管理三栏布局):按校区 List + Tree 扁平数据
|
||||||
|
const byCampus = await api("GET", "/api/classroom/List", { params: { campusId, page: 1, count: 20 } });
|
||||||
|
ok(
|
||||||
|
"List 按校区作用域命中 6 间",
|
||||||
|
byCampus.code === 0 && byCampus.data.total === 6,
|
||||||
|
`total=${byCampus.data && byCampus.data.total}`
|
||||||
|
);
|
||||||
|
ok(
|
||||||
|
"List 行含校区/楼栋名称",
|
||||||
|
byCampus.data.list.every(r => r.campusName === campusName && r.buildingName === `冒烟楼栋${stamp}`),
|
||||||
|
JSON.stringify(byCampus.data.list[0])
|
||||||
|
);
|
||||||
|
const byCampusFloor = await api("GET", "/api/classroom/List", { params: { campusId, floor: 1, page: 1, count: 20 } });
|
||||||
|
ok("List 校区+楼层作用域命中 3 间", byCampusFloor.code === 0 && byCampusFloor.data.total === 3);
|
||||||
|
ok("List 校区/楼栋均缺拒绝", (await api("GET", "/api/classroom/List", { params: { page: 1, count: 20 } })).code !== 0);
|
||||||
|
const treeRows = await api("GET", "/api/classroom/Tree");
|
||||||
|
const myTreeRows = treeRows.data ? treeRows.data.filter(r => r.buildingId === buildingId) : [];
|
||||||
|
ok(
|
||||||
|
"Tree 扁平数据含本楼栋 6 间(轻量字段)",
|
||||||
|
treeRows.code === 0 && myTreeRows.length === 6 && myTreeRows.every(r => r.campusId === campusId && r.roomCode && r.status),
|
||||||
|
JSON.stringify(myTreeRows[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 9.7 绑定班级:class 档案 → BindClass/UnbindClass/BatchUnbind → 年级 Tab 过滤
|
||||||
|
const clsNameA = `冒烟高一班${stamp}`;
|
||||||
|
const clsNameB = `冒烟高二班${stamp}`;
|
||||||
|
const clsAddA = await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1, studentCount: 45, headTeacher: "王老师" } });
|
||||||
|
ok("class Add(高一)", clsAddA.code === 0 && clsAddA.data > 0, clsAddA.msg);
|
||||||
|
const clsAddB = await api("POST", "/api/class/Add", { form: { className: clsNameB, grade: 2, studentCount: 44, headTeacher: "陈老师" } });
|
||||||
|
ok("class Add(高二)", clsAddB.code === 0 && clsAddB.data > 0, clsAddB.msg);
|
||||||
|
ok("class Add 校内重名拒绝", (await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1 } })).code !== 0);
|
||||||
|
ok("class Add 非法年级拒绝", (await api("POST", "/api/class/Add", { form: { className: `冒烟班X${stamp}`, grade: 9 } })).code !== 0);
|
||||||
|
const clsListG1 = await api("GET", "/api/class/List", { params: { grade: 1 } });
|
||||||
|
const clsA = clsListG1.data ? clsListG1.data.find(c => c.serID === clsAddA.data) : null;
|
||||||
|
ok("class List grade=1 含新班级(未绑定 classroomId=null)", clsListG1.code === 0 && clsA && clsA.classroomId == null && clsA.headTeacher === "王老师", JSON.stringify(clsA));
|
||||||
|
|
||||||
|
const bind = await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
|
||||||
|
ok("classroom BindClass 返回班级名", bind.code === 0 && bind.data && bind.data.className === clsNameA, bind.msg);
|
||||||
|
const bindDup = await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddA.data } });
|
||||||
|
ok("BindClass 班级已被其他教室占用拒绝", bindDup.code !== 0);
|
||||||
|
ok("BindClass 缺参拒绝", (await api("POST", "/api/classroom/BindClass", { json: { serID: room1 } })).code !== 0);
|
||||||
|
|
||||||
|
const roomsG1 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, page: 1, count: 20 } });
|
||||||
|
const boundRow = roomsG1.data ? roomsG1.data.list.find(r => r.serID === room1) : null;
|
||||||
|
ok(
|
||||||
|
"List grade=1 含已绑教室且行含班级信息(名/人数/班主任)",
|
||||||
|
roomsG1.code === 0 && roomsG1.data.total === 6 && boundRow && boundRow.classId === clsAddA.data &&
|
||||||
|
boundRow.className === clsNameA && boundRow.studentCount === 45 && boundRow.headTeacher === "王老师",
|
||||||
|
JSON.stringify(boundRow)
|
||||||
|
);
|
||||||
|
const roomsG2 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 2, page: 1, count: 20 } });
|
||||||
|
ok(
|
||||||
|
"List grade=2 排除高一已绑定教室(6-1=5,其余未绑定)",
|
||||||
|
roomsG2.code === 0 && roomsG2.data.total === 5 && roomsG2.data.list.every(r => r.serID !== room1 && r.classId == null),
|
||||||
|
`total=${roomsG2.data && roomsG2.data.total}`
|
||||||
|
);
|
||||||
|
const roomsBoundOnly = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, bindStatus: "bound", page: 1, count: 20 } });
|
||||||
|
ok("List bindStatus=bound 只含已绑 1 间", roomsBoundOnly.code === 0 && roomsBoundOnly.data.total === 1);
|
||||||
|
|
||||||
|
ok("class Delete 绑定中拒绝", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code !== 0);
|
||||||
|
|
||||||
|
ok("classroom UnbindClass", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
|
||||||
|
ok("UnbindClass 未绑定教室静默成功", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
|
||||||
|
|
||||||
|
// 批量绑定两间 → 批量解绑 3 间(含 1 间未绑定 → skipped)
|
||||||
|
await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
|
||||||
|
await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddB.data } });
|
||||||
|
const bUnbind = await api("POST", "/api/classroom/BatchUnbind", { json: [room1, rooms.data.list[1].serID, rooms.data.list[2].serID] });
|
||||||
|
ok(
|
||||||
|
"BatchUnbind 3 间(updated=2, skipped=1)",
|
||||||
|
bUnbind.code === 0 && bUnbind.data.updated === 2 && bUnbind.data.skipped === 1,
|
||||||
|
JSON.stringify(bUnbind.data)
|
||||||
|
);
|
||||||
|
ok("BatchUnbind 空集合拒绝", (await api("POST", "/api/classroom/BatchUnbind", { json: [] })).code !== 0);
|
||||||
|
ok("class Delete 解绑后可删 A", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code === 0);
|
||||||
|
ok("class Delete 解绑后可删 B", (await api("POST", "/api/class/Delete", { form: { serid: clsAddB.data } })).code === 0);
|
||||||
|
|
||||||
|
// 菜单兜底(仅本地免 SSO 模式):绑定班级在第二位
|
||||||
|
if (!COOKIE) {
|
||||||
|
const modsTree = await api("GET", "/api/base/GetUserMod");
|
||||||
|
const names = (modsTree.data || []).map(m => m.name);
|
||||||
|
ok(
|
||||||
|
"GetUserMod 菜单序 教室管理→绑定班级→基础设置",
|
||||||
|
modsTree.code === 0 && JSON.stringify(names) === JSON.stringify(["教室管理", "绑定班级", "基础设置"]),
|
||||||
|
JSON.stringify(names)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allIds = roomsAfterEdit.data.list.map(r => r.serID);
|
||||||
|
const bEditStatus = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds, status: "closed" } });
|
||||||
|
ok("classroom BatchEdit 批量关闭 6 间", bEditStatus.code === 0 && bEditStatus.data.updated === 6, bEditStatus.msg);
|
||||||
|
|
||||||
|
const afterBatch = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20, status: "closed" } });
|
||||||
|
ok("BatchEdit 后按 closed 过滤命中 6 间", afterBatch.code === 0 && afterBatch.data.total === 6);
|
||||||
|
|
||||||
|
const bEditType = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds.slice(0, 2), typeId } });
|
||||||
|
ok("classroom BatchEdit 批量改类型 2 间", bEditType.code === 0 && bEditType.data.updated === 2, bEditType.msg);
|
||||||
|
|
||||||
|
ok("BatchEdit 空集合拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [] } })).code !== 0);
|
||||||
|
ok("BatchEdit 未指定修改项拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1] } })).code !== 0);
|
||||||
|
ok(
|
||||||
|
"BatchEdit 非法状态拒绝",
|
||||||
|
(await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1], status: "xxx" } })).code !== 0
|
||||||
|
);
|
||||||
|
const bEditSkip = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1, 99999999], status: "open" } });
|
||||||
|
ok(
|
||||||
|
"BatchEdit 不存在 id 计入 skipped(不整批失败)",
|
||||||
|
bEditSkip.code === 0 && bEditSkip.data.updated === 1 && bEditSkip.data.skipped === 1,
|
||||||
|
JSON.stringify(bEditSkip.data)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 10. 其余删除守卫(楼栋有教室 → 类型被引用 → 校区有楼栋)
|
||||||
|
const delBuilding = await api("POST", "/api/building/Delete", { form: { serid: buildingId } });
|
||||||
|
ok("building Delete 守卫拒绝", delBuilding.code !== 0);
|
||||||
|
const delType = await api("POST", "/api/classtype/Delete", { form: { serid: typeId } });
|
||||||
|
ok("classtype Delete 守卫拒绝", delType.code !== 0);
|
||||||
|
const delCampus = await api("POST", "/api/campus/Delete", { form: { serid: campusId } });
|
||||||
|
ok("campus Delete 守卫拒绝", delCampus.code !== 0);
|
||||||
|
|
||||||
|
// 11. 逆序清理:教室(关联随删)→ 楼栋 → 类型 → 设备 → 校区
|
||||||
|
const delRooms = await api("POST", "/api/classroom/Delete", { json: allIds });
|
||||||
|
ok("classroom Delete 全部(关联自动清理)", delRooms.code === 0, delRooms.msg);
|
||||||
|
ok("equipment Delete(解除引用后可删)", (await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } })).code === 0);
|
||||||
|
ok("building Delete", (await api("POST", "/api/building/Delete", { form: { serid: buildingId } })).code === 0);
|
||||||
|
ok("classtype Delete", (await api("POST", "/api/classtype/Delete", { form: { serid: typeId } })).code === 0);
|
||||||
|
ok("campus Delete", (await api("POST", "/api/campus/Delete", { form: { serid: campusId } })).code === 0);
|
||||||
|
|
||||||
|
console.log(`\n[smoke] 通过 ${passed} / ${passed + failed}`);
|
||||||
|
process.exit(failed ? 1 : 0);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yunhai.acaclassroom;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.parser.ParserConfig;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.yunhai.acaclassroom")
|
||||||
|
@EnableTransactionManagement
|
||||||
|
public class AcaClassroomApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 开启 fastjson 安全模式
|
||||||
|
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||||
|
SpringApplication.run(AcaClassroomApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.acaclassroom.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注在控制器方法上,声明一次需要落审计日志的操作(决策 D2「保留日志表」→ 统一操作日志)。
|
||||||
|
* <p>由 {@link com.yunhai.acaclassroom.aspect.SysOperationLogAspect} 环绕拦截,写入 {@code sys_operation_log}。</p>
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface OperationLog {
|
||||||
|
|
||||||
|
/** 业务模块,如 "教室管理-校区" */
|
||||||
|
String module() default "";
|
||||||
|
|
||||||
|
/** 操作类型:CREATE / UPDATE / DELETE / QUERY(缺省按方法名启发式推断) */
|
||||||
|
String type() default "";
|
||||||
|
|
||||||
|
/** 日志级别:1 重要(删除) / 2 次要(新增/编辑) / 3 一般(查询)。缺省按 type 推断 */
|
||||||
|
int level() default 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.yunhai.acaclassroom.aspect;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.auth.BoaUserContext;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把基础平台的当前登录用户({@link BoaUser})复制到自有 {@link UserContext}(ThreadLocal),请求结束清理。
|
||||||
|
* <p>参照 UserclassService 的 ControllerUserContextAspect:优先从控制器入参里的 {@code BoaUser} 取,
|
||||||
|
* 否则回退到框架 {@code BoaUserContext#get()}。本模块为纯教师端,无视角切换。</p>
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(1)
|
||||||
|
public class ControllerUserContextAspect {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ControllerUserContextAspect.class);
|
||||||
|
|
||||||
|
/** 本地联调开关(默认关闭):无 SSO 会话时用固定开发身份填充 UserContext,便于无登录端到端联调。生产严禁开启。 */
|
||||||
|
@Value("${acaclassroom.dev-context.enabled:false}")
|
||||||
|
private boolean devContextEnabled;
|
||||||
|
|
||||||
|
@Around("execution(* com.yunhai.acaclassroom.controller..*.*(..))")
|
||||||
|
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
|
try {
|
||||||
|
BoaUser boaUser = resolveBoaUser(pjp.getArgs());
|
||||||
|
UserContext.UserInfo info = new UserContext.UserInfo();
|
||||||
|
if (boaUser != null) {
|
||||||
|
info.userId = boaUser.getId();
|
||||||
|
info.userName = boaUser.getName();
|
||||||
|
info.type = boaUser.getType();
|
||||||
|
if (boaUser.getCurrentRole() != null) {
|
||||||
|
info.schId = boaUser.getCurrentRole().getSchId();
|
||||||
|
} else if (boaUser.getSchool() != null) {
|
||||||
|
info.schId = boaUser.getSchool().getId();
|
||||||
|
}
|
||||||
|
if (boaUser.getRoleIds() != null && !boaUser.getRoleIds().isEmpty()) {
|
||||||
|
info.roleId = boaUser.getRoleIds().get(0);
|
||||||
|
}
|
||||||
|
} else if (devContextEnabled) {
|
||||||
|
info.userId = 99999;
|
||||||
|
info.userName = "本地开发";
|
||||||
|
info.schId = 1;
|
||||||
|
log.debug("[UserContext] 无 SSO 会话,已使用本地开发身份(schId=1)");
|
||||||
|
}
|
||||||
|
info.ip = clientIp();
|
||||||
|
UserContext.set(info);
|
||||||
|
return pjp.proceed();
|
||||||
|
} finally {
|
||||||
|
UserContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 控制器入参中若声明了 BoaUser(框架有参数解析器注入),优先用它;否则取 ThreadLocal 门面。 */
|
||||||
|
private BoaUser resolveBoaUser(Object[] args) {
|
||||||
|
if (args != null) {
|
||||||
|
for (Object a : args) {
|
||||||
|
if (a instanceof BoaUser) {
|
||||||
|
return (BoaUser) a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return BoaUserContext.get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[UserContext] 读取 BoaUserContext 失败: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServletRequest request() {
|
||||||
|
Object attrs = RequestContextHolder.getRequestAttributes();
|
||||||
|
return attrs instanceof ServletRequestAttributes ? ((ServletRequestAttributes) attrs).getRequest() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clientIp() {
|
||||||
|
HttpServletRequest req = request();
|
||||||
|
if (req == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String ip = req.getHeader("X-Forwarded-For");
|
||||||
|
if (ip != null && !ip.isEmpty()) {
|
||||||
|
int comma = ip.indexOf(',');
|
||||||
|
return comma > 0 ? ip.substring(0, comma).trim() : ip.trim();
|
||||||
|
}
|
||||||
|
ip = req.getHeader("X-Real-IP");
|
||||||
|
if (ip != null && !ip.isEmpty()) {
|
||||||
|
return ip.trim();
|
||||||
|
}
|
||||||
|
return req.getRemoteAddr();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package com.yunhai.acaclassroom.aspect;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.entity.SysOperationLog;
|
||||||
|
import com.yunhai.acaclassroom.service.ISysOperationLogService;
|
||||||
|
import com.yunhai.acaclassroom.utils.GzipUtil;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一操作日志切面(参照 UserclassService SysOperationLogAspect):
|
||||||
|
* 点切**全部控制器方法**,无需逐方法标注注解;
|
||||||
|
* <ul>
|
||||||
|
* <li>操作类型:方法名启发式(delete/del/remove→DELETE,add/create/insert/save/import/upload→CREATE,
|
||||||
|
* GET/查询→QUERY,其余→UPDATE);方法上的 {@link OperationLog} 可显式覆盖 type/level;</li>
|
||||||
|
* <li>入参:过滤 servlet/文件对象,JSON 序列化后正则脱敏(password/pwd/secret/token 等);</li>
|
||||||
|
* <li>响应:脱敏 + 截断后 GZIP 压缩存 {@code response_data};</li>
|
||||||
|
* <li>落库:走 {@code auditLogExecutor} 异步保存,失败仅告警,不影响主业务。</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>@Order 大于 {@link ControllerUserContextAspect},保证写日志时 {@link UserContext} 已填充。</p>
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(10)
|
||||||
|
public class SysOperationLogAspect {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SysOperationLogAspect.class);
|
||||||
|
|
||||||
|
/** JSON 键值脱敏(参照 UserclassService SensitiveDataUtil) */
|
||||||
|
private static final Pattern SENSITIVE_KEY = Pattern.compile(
|
||||||
|
"(\"(?:password|pwd|pass|secret|token|appsecret|accesskey|idcard|credential)\"\\s*:\\s*\")(.*?)(\")",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final int PARAMS_MAX = 2000;
|
||||||
|
private static final int RESPONSE_MAX = 10240;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysOperationLogService sysOperationLogService;
|
||||||
|
|
||||||
|
@Around("execution(* com.yunhai.acaclassroom.controller..*.*(..))")
|
||||||
|
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
int status = 0;
|
||||||
|
String errorMsg = null;
|
||||||
|
Object result = null;
|
||||||
|
try {
|
||||||
|
result = pjp.proceed();
|
||||||
|
return result;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
status = 1;
|
||||||
|
errorMsg = truncate(t.getMessage(), 2000);
|
||||||
|
throw t;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
save(pjp, result, status, errorMsg, System.currentTimeMillis() - start);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[SysOperationLog] 写入操作日志失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save(ProceedingJoinPoint pjp, Object result, int status, String errorMsg, long cost) {
|
||||||
|
SysOperationLog entity = new SysOperationLog();
|
||||||
|
MethodSignature sig = (MethodSignature) pjp.getSignature();
|
||||||
|
String methodName = sig.getName();
|
||||||
|
OperationLog ann = sig.getMethod().getAnnotation(OperationLog.class);
|
||||||
|
entity.setClassMethod(sig.getDeclaringType().getSimpleName() + "#" + methodName);
|
||||||
|
|
||||||
|
HttpServletRequest req = request();
|
||||||
|
String httpMethod = req != null ? req.getMethod() : "";
|
||||||
|
entity.setRequestUri(req != null ? req.getRequestURI() : null);
|
||||||
|
entity.setRequestMethod(httpMethod);
|
||||||
|
entity.setUserAgent(req != null ? truncate(req.getHeader("User-Agent"), 512) : null);
|
||||||
|
|
||||||
|
String opType = ann != null && !ann.type().isEmpty() ? ann.type() : resolveType(methodName, httpMethod);
|
||||||
|
entity.setOperationType(opType);
|
||||||
|
entity.setLogLevel(ann != null && ann.level() > 0 ? ann.level() : inferLevel(opType));
|
||||||
|
|
||||||
|
entity.setUserId(UserContext.getUserId());
|
||||||
|
entity.setUserName(UserContext.getUserName());
|
||||||
|
entity.setSchId(UserContext.getSchId());
|
||||||
|
entity.setRequestIp(UserContext.getIp());
|
||||||
|
entity.setRequestParams(maskArgs(pjp.getArgs()));
|
||||||
|
entity.setStatusCode(status);
|
||||||
|
entity.setCostTime(cost);
|
||||||
|
entity.setErrorMsg(errorMsg);
|
||||||
|
if (status == 0 && result != null && !"QUERY".equals(opType)) {
|
||||||
|
entity.setResponseData(GzipUtil.compress(truncate(maskJson(safeJson(result)), RESPONSE_MAX)));
|
||||||
|
}
|
||||||
|
sysOperationLogService.saveAsync(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 方法名启发式判定操作类型(参照 UserclassService OperationLogResolver)。 */
|
||||||
|
private String resolveType(String methodName, String httpMethod) {
|
||||||
|
String n = methodName.toLowerCase(Locale.ROOT);
|
||||||
|
if (n.contains("delete") || n.contains("del") || n.contains("remove")) {
|
||||||
|
return "DELETE";
|
||||||
|
}
|
||||||
|
if (n.startsWith("add") || n.startsWith("create") || n.startsWith("insert") || n.startsWith("save")
|
||||||
|
|| n.contains("import") || n.contains("upload") || n.startsWith("batch")) {
|
||||||
|
return "CREATE";
|
||||||
|
}
|
||||||
|
if (n.startsWith("get") || n.startsWith("list") || n.startsWith("query") || n.startsWith("find")
|
||||||
|
|| n.startsWith("stat") || "GET".equalsIgnoreCase(httpMethod)) {
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
return "UPDATE";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int inferLevel(String opType) {
|
||||||
|
if ("DELETE".equals(opType)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ("CREATE".equals(opType) || "UPDATE".equals(opType)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拼接可读入参,过滤 servlet/文件类对象并做简单脱敏。 */
|
||||||
|
private String maskArgs(Object[] args) {
|
||||||
|
if (args == null || args.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Object a : args) {
|
||||||
|
if (a == null || a instanceof HttpServletRequest || a instanceof HttpServletResponse
|
||||||
|
|| a instanceof MultipartFile || a instanceof org.springframework.data.domain.Pageable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String s;
|
||||||
|
try {
|
||||||
|
s = safeJson(a);
|
||||||
|
} catch (Exception e) {
|
||||||
|
s = "[unprintable]";
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append(s);
|
||||||
|
}
|
||||||
|
return truncate(sb.toString(), PARAMS_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeJson(Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (o.getClass().isArray()) {
|
||||||
|
return Arrays.deepToString((Object[]) o);
|
||||||
|
}
|
||||||
|
if (o instanceof String || o instanceof Number || o instanceof Boolean) {
|
||||||
|
return String.valueOf(o);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return maskJson(JSON.toJSONString(o));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return String.valueOf(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON 敏感键值替换为 ***(参照 UserclassService SensitiveDataUtil,绝不出错)。 */
|
||||||
|
private String maskJson(String json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Matcher m = SENSITIVE_KEY.matcher(json);
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
while (m.find()) {
|
||||||
|
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(1) + "***" + m.group(3)));
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServletRequest request() {
|
||||||
|
Object attrs = RequestContextHolder.getRequestAttributes();
|
||||||
|
return attrs instanceof ServletRequestAttributes ? ((ServletRequestAttributes) attrs).getRequest() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String s, int max) {
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return s.length() <= max ? s : s.substring(0, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.yunhai.acaclassroom.common.listener;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.BaseEntity;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.persistence.PrePersist;
|
||||||
|
import javax.persistence.PreUpdate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 审计监听器:实体持久化/更新前自动填充创建人/更新人与时间戳。
|
||||||
|
* <p>决策 D4:{@code create_by / update_by} 存**当前登录用户 ID**(数字 id),不再存登录账号。
|
||||||
|
* id 取自 {@link UserContext#getUserId()}(由 ControllerUserContextAspect 在请求线程填充);
|
||||||
|
* 无登录上下文(启动种子/系统任务)时为 null,保持原值。</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AuditEntityListener {
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist(BaseEntity entity) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
entity.setCreateTime(now);
|
||||||
|
entity.setUpdateTime(now);
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
if (uid != null) {
|
||||||
|
if (entity.getCreateBy() == null) {
|
||||||
|
entity.setCreateBy(uid);
|
||||||
|
}
|
||||||
|
entity.setUpdateBy(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate(BaseEntity entity) {
|
||||||
|
entity.setUpdateTime(LocalDateTime.now());
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
if (uid != null) {
|
||||||
|
entity.setUpdateBy(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前登录用户 ID(数字);无上下文返回 null。 */
|
||||||
|
private Integer currentUserId() {
|
||||||
|
return UserContext.getUserId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yunhai.acaclassroom.common.validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||||
|
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||||
|
*/
|
||||||
|
public interface ValidationGroups {
|
||||||
|
interface Create {}
|
||||||
|
interface Update {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yunhai.acaclassroom.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志异步落库线程池(参照 UserclassService auditLogExecutor)。
|
||||||
|
* <p>队列打满时丢弃任务(DiscardPolicy):审计日志让位于主业务,绝不阻塞请求。</p>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean("auditLogExecutor")
|
||||||
|
public Executor auditLogExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(8);
|
||||||
|
executor.setQueueCapacity(2000);
|
||||||
|
executor.setThreadNamePrefix("audit-log-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.yunhai.acaclassroom.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局路径常量。与 UserBase 一致:API 统一挂在 /{platform}/ 之下(platform = api / manage / mobiapi)。
|
||||||
|
*/
|
||||||
|
public enum Config {
|
||||||
|
Inst;
|
||||||
|
|
||||||
|
public static final String API_PATH = "/{platform}"; // manage / api / mobiapi,前面有/后面无/,拼接子路径时用"/"分隔
|
||||||
|
public static final String OPEN_API_PATH = "/oapi";
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.acaclassroom.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson 时间序列化全局约定(对齐旧 .NET 版 "yyyy-MM-dd HH:mm:ss"):
|
||||||
|
* 接口出入参中的 LocalDateTime/LocalDate 统一使用该格式,避免 ISO 的 "T" 分隔符
|
||||||
|
* (如 2026-09-01T16:38:40)直接暴露给前端表格。
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||||
|
return builder -> builder
|
||||||
|
.serializers(
|
||||||
|
new LocalDateTimeSerializer(DATE_TIME),
|
||||||
|
new LocalDateSerializer(DATE))
|
||||||
|
.deserializers(
|
||||||
|
new LocalDateTimeDeserializer(DATE_TIME),
|
||||||
|
new LocalDateDeserializer(DATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.acaclassroom.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口文档(springdoc-openapi)。
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI acaClassroomOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("教室管理接口文档")
|
||||||
|
.description("教室管理子应用:校区、楼栋、教室类型、教室管理(springdoc-openapi)")
|
||||||
|
.version("1.0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.IUserCenterService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台代理域接口(base/)。
|
||||||
|
* <p>路径前缀:{@code Config.API_PATH + "/base/"}。
|
||||||
|
* 承载平台代理接口:GetUserInfo / GetUserMod,业务逻辑已下沉 {@link IUserCenterService}。
|
||||||
|
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||||
|
public class BaseApiController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUserCenterService userCenterService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息(顶栏姓名/头像数据源)。
|
||||||
|
* <p>CAS 会话身份优先;无会话且 dev-context 开启时按 {@code acaclassroom.dev-context.login-name}
|
||||||
|
* 查平台用户中心返回真实用户(免 SSO 联调也能拿到 lijing 等账号的姓名与头像)。
|
||||||
|
* 两者皆无时返回 null,由前端降级显示默认头像与"管理员"。</p>
|
||||||
|
*/
|
||||||
|
@GetMapping("GetUserInfo")
|
||||||
|
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||||
|
return userCenterService.getCurrentUser(authUserWithDept);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户已授权的模块列表(左侧菜单树)。
|
||||||
|
* 模块树组装已下沉 IUserCenterService;modId 与前端路由 meta.modId 对齐(见计划文档 D6)。
|
||||||
|
* 无 CAS 会话(本地联调)时回退 UserContext(acaclassroom.dev-context 填充的开发身份)。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetUserMod")
|
||||||
|
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
|
||||||
|
Integer userId;
|
||||||
|
Integer schId;
|
||||||
|
if (authUserWithDept != null) {
|
||||||
|
userId = authUserWithDept.getId();
|
||||||
|
schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
} else {
|
||||||
|
userId = UserContext.getUserId();
|
||||||
|
schId = UserContext.getSchId();
|
||||||
|
}
|
||||||
|
return userCenterService.getUserModuleTree(userId, schId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.IBuildingService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋管理接口(building/)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/building/")
|
||||||
|
public class BuildingController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IBuildingService buildingService;
|
||||||
|
|
||||||
|
/** 楼栋列表(campusId 可空=全部)。 */
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<Map<String, Object>> list(@RequestParam(value = "campusId", required = false) Integer campusId) {
|
||||||
|
return buildingService.list(campusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 楼栋统计(教室管理页"显示图")。 */
|
||||||
|
@GetMapping("Stats")
|
||||||
|
public List<Map<String, Object>> stats(@RequestParam(value = "campusId", required = false) Integer campusId) {
|
||||||
|
return buildingService.stats(campusId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "教室管理-楼栋", type = "CREATE")
|
||||||
|
public Integer add(@RequestParam("campusId") Integer campusId,
|
||||||
|
@RequestParam("buildingName") String buildingName,
|
||||||
|
@RequestParam("buildingCode") String buildingCode,
|
||||||
|
@RequestParam("floorCount") Integer floorCount,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
return buildingService.add(campusId, buildingName, buildingCode, floorCount, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "教室管理-楼栋", type = "UPDATE")
|
||||||
|
public void edit(@RequestParam("serID") Integer serID,
|
||||||
|
@RequestParam("campusId") Integer campusId,
|
||||||
|
@RequestParam("buildingName") String buildingName,
|
||||||
|
@RequestParam("buildingCode") String buildingCode,
|
||||||
|
@RequestParam("floorCount") Integer floorCount,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
buildingService.edit(serID, campusId, buildingName, buildingCode, floorCount, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-楼栋", type = "DELETE")
|
||||||
|
public void delete(@RequestParam("serid") Integer serid) {
|
||||||
|
buildingService.delete(serid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.ICampusService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校区管理接口(campus/)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/campus/")
|
||||||
|
public class CampusController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ICampusService campusService;
|
||||||
|
|
||||||
|
/** 校区列表(含楼栋数/教室数)。 */
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<Map<String, Object>> list() {
|
||||||
|
return campusService.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校区统计(总览页)。 */
|
||||||
|
@GetMapping("Stats")
|
||||||
|
public List<Map<String, Object>> stats() {
|
||||||
|
return campusService.stats();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "教室管理-校区", type = "CREATE")
|
||||||
|
public Integer add(@RequestParam("campusName") String campusName,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
return campusService.add(campusName, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "教室管理-校区", type = "UPDATE")
|
||||||
|
public void edit(@RequestParam("serID") Integer serID,
|
||||||
|
@RequestParam("campusName") String campusName,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
campusService.edit(serID, campusName, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-校区", type = "DELETE")
|
||||||
|
public void delete(@RequestParam("serid") Integer serid) {
|
||||||
|
campusService.delete(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 ids 顺序重排(sortOrder = 序号)。 */
|
||||||
|
@PostMapping("Sort")
|
||||||
|
@OperationLog(module = "教室管理-校区", type = "UPDATE")
|
||||||
|
public void sort(@RequestParam("ids") String ids) {
|
||||||
|
campusService.sort(parseIds(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Integer> parseIds(String ids) {
|
||||||
|
if (ids == null || ids.trim().isEmpty()) {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String[] parts = ids.split(",");
|
||||||
|
List<Integer> result = new java.util.ArrayList<>();
|
||||||
|
for (String p : parts) {
|
||||||
|
if (!p.trim().isEmpty()) {
|
||||||
|
result.add(Integer.valueOf(p.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "ids 格式不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.IClassService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级档案接口(class/,绑定班级页数据源)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/class/")
|
||||||
|
public class ClassController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IClassService classService;
|
||||||
|
|
||||||
|
/** 班级列表(全校,可按年级过滤;行含 classroomId=绑定的教室 id,未绑定为 null)。 */
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<Map<String, Object>> list(@RequestParam(value = "grade", required = false) Integer grade) {
|
||||||
|
return classService.list(grade);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "教室管理-绑定班级", type = "CREATE")
|
||||||
|
public Integer add(@RequestParam("className") String className,
|
||||||
|
@RequestParam("grade") Integer grade,
|
||||||
|
@RequestParam(value = "studentCount", required = false) Integer studentCount,
|
||||||
|
@RequestParam(value = "headTeacher", required = false) String headTeacher) {
|
||||||
|
return classService.add(className, grade, studentCount, headTeacher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除班级(已被教室绑定时拒绝)。 */
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-绑定班级", type = "DELETE")
|
||||||
|
public void delete(@RequestParam("serid") Integer serid) {
|
||||||
|
classService.delete(serid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchAddRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBindRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchEditRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomEditRequest;
|
||||||
|
import com.yunhai.acaclassroom.service.IClassroomService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室管理接口(classroom/)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/classroom/")
|
||||||
|
public class ClassroomController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IClassroomService classroomService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室列表(空间作用域:楼栋或校区二选一,楼栋优先;
|
||||||
|
* 支持楼层/类型/状态/关键字/年级/绑定状态过滤 + 排序 + 分页)。
|
||||||
|
*/
|
||||||
|
@GetMapping("List")
|
||||||
|
public Map<String, Object> list(@RequestParam(value = "buildingId", required = false) Integer buildingId,
|
||||||
|
@RequestParam(value = "campusId", required = false) Integer campusId,
|
||||||
|
@RequestParam(value = "floor", required = false) Integer floor,
|
||||||
|
@RequestParam(value = "typeId", required = false) Integer typeId,
|
||||||
|
@RequestParam(value = "status", required = false) String status,
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
|
@RequestParam(value = "grade", required = false) Integer grade,
|
||||||
|
@RequestParam(value = "bindStatus", required = false) String bindStatus,
|
||||||
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "count", defaultValue = "20") Integer count,
|
||||||
|
@RequestParam(value = "sortField", required = false) String sortField,
|
||||||
|
@RequestParam(value = "order", required = false) String order) {
|
||||||
|
return classroomService.list(buildingId, campusId, floor, typeId, status, keyword, grade, bindStatus,
|
||||||
|
page, count, sortField, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 楼栋下已有教室的楼层列表(筛选项)。 */
|
||||||
|
@GetMapping("Floors")
|
||||||
|
public List<Integer> floors(@RequestParam("buildingId") Integer buildingId) {
|
||||||
|
return classroomService.floors(buildingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全校教室扁平列表(教室管理页「空间筛选树」数据源,轻量字段)。 */
|
||||||
|
@GetMapping("Tree")
|
||||||
|
public List<Map<String, Object>> tree() {
|
||||||
|
return classroomService.tree();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加教室:输入楼层区间(1~楼栋楼层数量)、每层教室数量、教室类型;
|
||||||
|
* 编号默认 {前缀}{楼层2位}{序号2位},重复的跳过。
|
||||||
|
*/
|
||||||
|
@PostMapping("BatchAdd")
|
||||||
|
@OperationLog(module = "教室管理-教室", type = "CREATE")
|
||||||
|
public Map<String, Object> batchAdd(@RequestBody ClassroomBatchAddRequest req) {
|
||||||
|
return classroomService.batchAdd(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑教室(类型/名称/编号/状态/容纳人数/座位数)。 */
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "教室管理-教室", type = "UPDATE")
|
||||||
|
public void edit(@RequestBody ClassroomEditRequest req) {
|
||||||
|
classroomService.edit(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量编辑教室:仅支持 教室状态 / 教室类型 两项(可只改其一),
|
||||||
|
* 供列表页多选后的批量操作条使用;返回 {updated, skipped, total}。
|
||||||
|
*/
|
||||||
|
@PostMapping("BatchEdit")
|
||||||
|
@OperationLog(module = "教室管理-教室", type = "UPDATE")
|
||||||
|
public Map<String, Object> batchEdit(@RequestBody ClassroomBatchEditRequest req) {
|
||||||
|
return classroomService.batchEdit(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除教室(JSON id 数组)。 */
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-教室", type = "DELETE")
|
||||||
|
public void delete(@RequestBody List<Integer> ids) {
|
||||||
|
classroomService.delete(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室绑定班级(JSON {serID, classId};一个班级同时只能绑一间教室)。
|
||||||
|
*/
|
||||||
|
@PostMapping("BindClass")
|
||||||
|
@OperationLog(module = "教室管理-绑定班级", type = "UPDATE")
|
||||||
|
public Map<String, Object> bindClass(@RequestBody ClassroomBindRequest req) {
|
||||||
|
return classroomService.bindClass(req.getSerID(), req.getClassId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 教室解绑班级(表单 serid;未绑定时静默成功)。 */
|
||||||
|
@PostMapping("UnbindClass")
|
||||||
|
@OperationLog(module = "教室管理-绑定班级", type = "UPDATE")
|
||||||
|
public void unbindClass(@RequestParam("serid") Integer serid) {
|
||||||
|
classroomService.unbindClass(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量解绑(JSON id 数组;未绑定/无效教室计入 skipped)。 */
|
||||||
|
@PostMapping("BatchUnbind")
|
||||||
|
@OperationLog(module = "教室管理-绑定班级", type = "UPDATE")
|
||||||
|
public Map<String, Object> batchUnbind(@RequestBody List<Integer> serIDs) {
|
||||||
|
return classroomService.batchUnbind(serIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.IClassroomTypeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室类型管理接口(classtype/)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/classtype/")
|
||||||
|
public class ClassroomTypeController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IClassroomTypeService classroomTypeService;
|
||||||
|
|
||||||
|
/** 类型列表(含引用的教室数)。 */
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<Map<String, Object>> list() {
|
||||||
|
return classroomTypeService.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "教室管理-教室类型", type = "CREATE")
|
||||||
|
public Integer add(@RequestParam("typeName") String typeName,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
return classroomTypeService.add(typeName, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "教室管理-教室类型", type = "UPDATE")
|
||||||
|
public void edit(@RequestParam("serID") Integer serID,
|
||||||
|
@RequestParam("typeName") String typeName,
|
||||||
|
@RequestParam(value = "enableFlag", defaultValue = "1") Integer enableFlag,
|
||||||
|
@RequestParam(value = "sortOrder", required = false) Integer sortOrder) {
|
||||||
|
classroomTypeService.edit(serID, typeName, enableFlag, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-教室类型", type = "DELETE")
|
||||||
|
public void delete(@RequestParam("serid") Integer serid) {
|
||||||
|
classroomTypeService.delete(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 ids 顺序重排。 */
|
||||||
|
@PostMapping("Sort")
|
||||||
|
@OperationLog(module = "教室管理-教室类型", type = "UPDATE")
|
||||||
|
public void sort(@RequestParam("ids") String ids) {
|
||||||
|
classroomTypeService.sort(CampusController.parseIds(ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.yunhai.acaclassroom.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.annotation.OperationLog;
|
||||||
|
import com.yunhai.acaclassroom.config.Config;
|
||||||
|
import com.yunhai.acaclassroom.service.IEquipmentService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室设备管理接口(equipment/,基础设置 · 设备管理)。
|
||||||
|
* 独立设备台账:设备不挂教室,教室(批量添加/编辑)通过 classroom/ 接口勾选设备。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/equipment/")
|
||||||
|
public class EquipmentController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IEquipmentService equipmentService;
|
||||||
|
|
||||||
|
/** 设备列表(全校全量,支持关键字过滤,行含统计使用数量 quantity 与引用教室数 classroomCount)。 */
|
||||||
|
@GetMapping("List")
|
||||||
|
public Map<String, Object> list(@RequestParam(value = "keyword", required = false) String keyword) {
|
||||||
|
return equipmentService.list(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "教室管理-设备", type = "CREATE")
|
||||||
|
public Integer add(@RequestParam("equipmentName") String equipmentName,
|
||||||
|
@RequestParam(value = "remark", required = false) String remark) {
|
||||||
|
return equipmentService.add(equipmentName, remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "教室管理-设备", type = "UPDATE")
|
||||||
|
public void edit(@RequestParam("serID") Integer serID,
|
||||||
|
@RequestParam("equipmentName") String equipmentName,
|
||||||
|
@RequestParam(value = "remark", required = false) String remark) {
|
||||||
|
equipmentService.edit(serID, equipmentName, remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "教室管理-设备", type = "DELETE")
|
||||||
|
public void delete(@RequestParam("serid") Integer serid) {
|
||||||
|
equipmentService.delete(serid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.common.listener.AuditEntityListener;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link AuditEntityListener} 自动填充)。
|
||||||
|
*/
|
||||||
|
@MappedSuperclass
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(AuditEntityListener.class)
|
||||||
|
public abstract class BaseEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人用户ID(数字;决策 D4,不存登录账号)
|
||||||
|
*/
|
||||||
|
@Column(name = "create_by", updatable = false)
|
||||||
|
private Integer createBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@Column(name = "create_time", updatable = false)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人用户ID(数字;决策 D4,不存登录账号)
|
||||||
|
*/
|
||||||
|
@Column(name = "update_by")
|
||||||
|
private Integer updateBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@Column(name = "update_time")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋(基础设置):所属校区 + 名称/编号/楼层数量/排序号。楼栋编号校内唯一。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_building")
|
||||||
|
public class Building extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属校区 id(aca_campus.id) */
|
||||||
|
@Column(name = "campus_id", nullable = false)
|
||||||
|
private Integer campusId;
|
||||||
|
|
||||||
|
/** 楼栋名称 */
|
||||||
|
@Column(name = "building_name", nullable = false, length = 100)
|
||||||
|
private String buildingName;
|
||||||
|
|
||||||
|
/** 楼栋编号(校内唯一,批量生成教室编号的前缀) */
|
||||||
|
@Column(name = "building_code", nullable = false, length = 32)
|
||||||
|
private String buildingCode;
|
||||||
|
|
||||||
|
/** 楼层数量(教室楼层不得超过该值) */
|
||||||
|
@Column(name = "floor_count", nullable = false)
|
||||||
|
private Integer floorCount;
|
||||||
|
|
||||||
|
/** 排序号(小在前) */
|
||||||
|
@Column(name = "sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校区(基础设置)。多租户按 sch_id 隔离,校区名校内唯一。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_campus")
|
||||||
|
public class Campus extends BaseEntity {
|
||||||
|
|
||||||
|
/** 校区名称(校内唯一) */
|
||||||
|
@Column(name = "campus_name", nullable = false, length = 100)
|
||||||
|
private String campusName;
|
||||||
|
|
||||||
|
/** 排序号(小在前) */
|
||||||
|
@Column(name = "sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室。campus_id 随楼栋冗余存储(校区调整楼栋时同步),楼栋内教室编号唯一。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_classroom")
|
||||||
|
public class Classroom extends BaseEntity {
|
||||||
|
|
||||||
|
/** 所属校区 id(冗余自楼栋,列表/统计按校区过滤免联表) */
|
||||||
|
@Column(name = "campus_id", nullable = false)
|
||||||
|
private Integer campusId;
|
||||||
|
|
||||||
|
/** 所属楼栋 id(aca_building.id) */
|
||||||
|
@Column(name = "building_id", nullable = false)
|
||||||
|
private Integer buildingId;
|
||||||
|
|
||||||
|
/** 所在楼层(1..楼栋楼层数量) */
|
||||||
|
@Column(name = "floor", nullable = false)
|
||||||
|
private Integer floor;
|
||||||
|
|
||||||
|
/** 教室名称 */
|
||||||
|
@Column(name = "room_name", nullable = false, length = 100)
|
||||||
|
private String roomName;
|
||||||
|
|
||||||
|
/** 教室编号(楼栋内唯一,默认 楼栋编号+楼层2位+序号2位,如 A0103) */
|
||||||
|
@Column(name = "room_code", nullable = false, length = 32)
|
||||||
|
private String roomCode;
|
||||||
|
|
||||||
|
/** 教室类型 id(aca_classroom_type.id) */
|
||||||
|
@Column(name = "type_id")
|
||||||
|
private Integer typeId;
|
||||||
|
|
||||||
|
/** 教室状态:open 开放 / closed 关闭 */
|
||||||
|
@Column(name = "status", nullable = false, length = 8)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 教室容纳人数 */
|
||||||
|
@Column(name = "capacity")
|
||||||
|
private Integer capacity;
|
||||||
|
|
||||||
|
/** 座位数 */
|
||||||
|
@Column(name = "seat_count")
|
||||||
|
private Integer seatCount;
|
||||||
|
|
||||||
|
/** 绑定班级 id(aca_class.id,可空=未绑定;同一班级同时只能绑一间教室) */
|
||||||
|
@Column(name = "class_id")
|
||||||
|
private Integer classId;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室-设备关联(教室编辑/批量添加时勾选的设备集合)。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_classroom_equipment",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"classroom_id", "equipment_id"}))
|
||||||
|
public class ClassroomEquipment extends BaseEntity {
|
||||||
|
|
||||||
|
/** 教室 id(aca_classroom.id) */
|
||||||
|
@Column(name = "classroom_id", nullable = false)
|
||||||
|
private Integer classroomId;
|
||||||
|
|
||||||
|
/** 设备 id(aca_equipment.id) */
|
||||||
|
@Column(name = "equipment_id", nullable = false)
|
||||||
|
private Integer equipmentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每间教室的设备数量(批量添加/编辑时按设备指定,缺省 1)。
|
||||||
|
* 列为后加:ddl-auto 建出的存量行可能为 NULL,读取时按 1 处理。
|
||||||
|
*/
|
||||||
|
@Column(name = "quantity")
|
||||||
|
private Integer quantity;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室类型(基础设置)。类型名校内唯一;停用后不可再用于新增教室,已有教室不受影响。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_classroom_type")
|
||||||
|
public class ClassroomType extends BaseEntity {
|
||||||
|
|
||||||
|
/** 类型名称(校内唯一,如 普通教室/多媒体教室/实验室) */
|
||||||
|
@Column(name = "type_name", nullable = false, length = 50)
|
||||||
|
private String typeName;
|
||||||
|
|
||||||
|
/** 启用标志:1 启用 / 0 停用 */
|
||||||
|
@Column(name = "enable_flag", nullable = false)
|
||||||
|
private Integer enableFlag;
|
||||||
|
|
||||||
|
/** 排序号(小在前) */
|
||||||
|
@Column(name = "sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室设备(基础设置 · 设备管理)。独立设备台账,不挂教室;
|
||||||
|
* 教室通过 {@link ClassroomEquipment} 关联引用设备(教室批量添加/编辑时勾选)。
|
||||||
|
* 设备不存数量,使用量按教室关联的 quantity 汇总统计。
|
||||||
|
* 设备名称校内唯一;被教室引用的设备不可删除(删除守卫)。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_equipment")
|
||||||
|
public class Equipment extends BaseEntity {
|
||||||
|
|
||||||
|
/** 设备名称(校内唯一,如 投影仪/空调/多媒体讲台) */
|
||||||
|
@Column(name = "equipment_name", nullable = false, length = 100)
|
||||||
|
private String equipmentName;
|
||||||
|
|
||||||
|
/** 备注 */
|
||||||
|
@Column(name = "remark", length = 200)
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级档案(绑定班级页数据源)。班名校内唯一;教室经 aca_classroom.class_id 引用。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "aca_class")
|
||||||
|
public class SchoolClass extends BaseEntity {
|
||||||
|
|
||||||
|
/** 年级:1=高一 2=高二 3=高三 */
|
||||||
|
public static final int GRADE_1 = 1;
|
||||||
|
public static final int GRADE_2 = 2;
|
||||||
|
public static final int GRADE_3 = 3;
|
||||||
|
|
||||||
|
/** 班级名称(如 高一1班),校内唯一 */
|
||||||
|
@Column(name = "class_name", nullable = false, length = 50)
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
/** 年级:1=高一 2=高二 3=高三 */
|
||||||
|
@Column(name = "grade", nullable = false)
|
||||||
|
private Integer grade;
|
||||||
|
|
||||||
|
/** 学生人数 */
|
||||||
|
@Column(name = "student_count", nullable = false)
|
||||||
|
private Integer studentCount;
|
||||||
|
|
||||||
|
/** 班主任姓名 */
|
||||||
|
@Column(name = "head_teacher", length = 50)
|
||||||
|
private String headTeacher;
|
||||||
|
|
||||||
|
/** 所属学校 id(多租户) */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.yunhai.acaclassroom.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一操作日志(审计轨)。参照 UserclassService {@code sys_operation_log}。
|
||||||
|
* <p>由 {@link com.yunhai.acaclassroom.aspect.SysOperationLogAspect} 对全部控制器方法环绕落库
|
||||||
|
* (写操作可加 {@code @OperationLog} 显式指定模块/类型)。</p>
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_operation_log")
|
||||||
|
public class SysOperationLog implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 1 重要(删除) / 2 次要(新增/编辑) / 3 一般(查询) */
|
||||||
|
@Column(name = "log_level", nullable = false)
|
||||||
|
private Integer logLevel;
|
||||||
|
|
||||||
|
/** DELETE / CREATE / UPDATE / QUERY */
|
||||||
|
@Column(name = "operation_type", nullable = false, length = 16)
|
||||||
|
private String operationType;
|
||||||
|
|
||||||
|
/** 操作人平台用户 id(来自 UserContext,非账号) */
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
/** 操作人姓名 */
|
||||||
|
@Column(name = "user_name", length = 64)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
/** 学校 id(多租户隔离) */
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
|
||||||
|
@Column(name = "request_ip", length = 64)
|
||||||
|
private String requestIp;
|
||||||
|
|
||||||
|
@Column(name = "request_uri", length = 512)
|
||||||
|
private String requestUri;
|
||||||
|
|
||||||
|
@Column(name = "request_method", length = 16)
|
||||||
|
private String requestMethod;
|
||||||
|
|
||||||
|
/** 目标类#方法 */
|
||||||
|
@Column(name = "class_method", length = 512)
|
||||||
|
private String classMethod;
|
||||||
|
|
||||||
|
/** 入参(脱敏后),截断 */
|
||||||
|
@Column(name = "request_params", columnDefinition = "TEXT")
|
||||||
|
private String requestParams;
|
||||||
|
|
||||||
|
/** 响应(脱敏 + 截断后 GZIP 压缩),MEDIUMBLOB */
|
||||||
|
@Lob
|
||||||
|
@Column(name = "response_data", columnDefinition = "MEDIUMBLOB")
|
||||||
|
private byte[] responseData;
|
||||||
|
|
||||||
|
/** 业务状态:0 成功 / 非 0 失败 */
|
||||||
|
@Column(name = "status_code", nullable = false)
|
||||||
|
private Integer statusCode;
|
||||||
|
|
||||||
|
/** 耗时(毫秒) */
|
||||||
|
@Column(name = "cost_time", nullable = false)
|
||||||
|
private Long costTime;
|
||||||
|
|
||||||
|
@Column(name = "user_agent", length = 512)
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
/** 异常信息,截断 2000 */
|
||||||
|
@Column(name = "error_msg", columnDefinition = "TEXT")
|
||||||
|
private String errorMsg;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "create_time", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.yunhai.acaclassroom.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室批量添加请求(JSON)。
|
||||||
|
* <p>按楼层区间逐层生成教室:楼层从 floorFrom 到 floorTo(含),每层 countPerFloor 间;
|
||||||
|
* 编号默认 {楼栋编号}{楼层2位}{序号2位},与已有编号冲突的跳过。</p>
|
||||||
|
*/
|
||||||
|
public class ClassroomBatchAddRequest {
|
||||||
|
|
||||||
|
private Integer buildingId;
|
||||||
|
|
||||||
|
/** 起始楼层(≥1) */
|
||||||
|
private Integer floorFrom;
|
||||||
|
|
||||||
|
/** 截止楼层(≤楼栋楼层数量,≥floorFrom) */
|
||||||
|
private Integer floorTo;
|
||||||
|
|
||||||
|
/** 每层教室数量(≥1) */
|
||||||
|
private Integer countPerFloor;
|
||||||
|
|
||||||
|
/** 教室类型 id */
|
||||||
|
private Integer typeId;
|
||||||
|
|
||||||
|
/** 编号/名称前缀(缺省用楼栋编号) */
|
||||||
|
private String namePrefix;
|
||||||
|
|
||||||
|
/** 关联设备 id 集合(应用到本次新生成的教室,数量按 1;可空)。优先级低于 equipments */
|
||||||
|
private java.util.List<Integer> equipmentIds;
|
||||||
|
|
||||||
|
/** 关联设备(应用到本次新生成的教室,含每间数量;可空) */
|
||||||
|
private java.util.List<ClassroomEquipmentItem> equipments;
|
||||||
|
|
||||||
|
public java.util.List<ClassroomEquipmentItem> getEquipments() {
|
||||||
|
return equipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEquipments(java.util.List<ClassroomEquipmentItem> equipments) {
|
||||||
|
this.equipments = equipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<Integer> getEquipmentIds() {
|
||||||
|
return equipmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEquipmentIds(java.util.List<Integer> equipmentIds) {
|
||||||
|
this.equipmentIds = equipmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getBuildingId() {
|
||||||
|
return buildingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuildingId(Integer buildingId) {
|
||||||
|
this.buildingId = buildingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFloorFrom() {
|
||||||
|
return floorFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFloorFrom(Integer floorFrom) {
|
||||||
|
this.floorFrom = floorFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFloorTo() {
|
||||||
|
return floorTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFloorTo(Integer floorTo) {
|
||||||
|
this.floorTo = floorTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCountPerFloor() {
|
||||||
|
return countPerFloor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCountPerFloor(Integer countPerFloor) {
|
||||||
|
this.countPerFloor = countPerFloor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTypeId() {
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeId(Integer typeId) {
|
||||||
|
this.typeId = typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNamePrefix() {
|
||||||
|
return namePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNamePrefix(String namePrefix) {
|
||||||
|
this.namePrefix = namePrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.yunhai.acaclassroom.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室批量编辑请求(JSON)。
|
||||||
|
*
|
||||||
|
* 用于列表页多选后的批量操作:只允许修改「教室状态」与「教室类型」两项;
|
||||||
|
* 两者均为可空——为 null 表示该项不改动,至少需要指定其中一项。
|
||||||
|
* 名称/编号/容纳人数/座位数等逐间差异字段不在批量范围(避免误覆盖)。
|
||||||
|
*/
|
||||||
|
public class ClassroomBatchEditRequest {
|
||||||
|
|
||||||
|
/** 目标教室 id 集合 */
|
||||||
|
private List<Integer> serIDs;
|
||||||
|
|
||||||
|
/** 目标状态:open 开放 / closed 关闭;null = 不改动 */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/** 目标教室类型 id;null = 不改动 */
|
||||||
|
private Integer typeId;
|
||||||
|
|
||||||
|
public List<Integer> getSerIDs() {
|
||||||
|
return serIDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSerIDs(List<Integer> serIDs) {
|
||||||
|
this.serIDs = serIDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTypeId() {
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeId(Integer typeId) {
|
||||||
|
this.typeId = typeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yunhai.acaclassroom.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室绑定班级请求(绑定班级页行内下拉)。
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ClassroomBindRequest {
|
||||||
|
|
||||||
|
/** 教室 id */
|
||||||
|
private Integer serID;
|
||||||
|
|
||||||
|
/** 班级 id(aca_class.id) */
|
||||||
|
private Integer classId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.yunhai.acaclassroom.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室编辑请求(JSON)。楼层/楼栋/校区为档案归属,不在编辑范围。
|
||||||
|
*/
|
||||||
|
public class ClassroomEditRequest {
|
||||||
|
|
||||||
|
private Integer serID;
|
||||||
|
|
||||||
|
private String roomName;
|
||||||
|
|
||||||
|
private String roomCode;
|
||||||
|
|
||||||
|
private Integer typeId;
|
||||||
|
|
||||||
|
/** open 开放 / closed 关闭 */
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private Integer capacity;
|
||||||
|
|
||||||
|
private Integer seatCount;
|
||||||
|
|
||||||
|
/** 关联设备 id 集合(null=不改动;数组(含空)=全量替换该教室的设备,数量按 1)。优先级低于 equipments */
|
||||||
|
private java.util.List<Integer> equipmentIds;
|
||||||
|
|
||||||
|
/** 关联设备(null=不改动;数组(含空)=全量替换该教室的设备,含每间数量) */
|
||||||
|
private java.util.List<ClassroomEquipmentItem> equipments;
|
||||||
|
|
||||||
|
public java.util.List<ClassroomEquipmentItem> getEquipments() {
|
||||||
|
return equipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEquipments(java.util.List<ClassroomEquipmentItem> equipments) {
|
||||||
|
this.equipments = equipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<Integer> getEquipmentIds() {
|
||||||
|
return equipmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEquipmentIds(java.util.List<Integer> equipmentIds) {
|
||||||
|
this.equipmentIds = equipmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSerID() {
|
||||||
|
return serID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSerID(Integer serID) {
|
||||||
|
this.serID = serID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoomName() {
|
||||||
|
return roomName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoomName(String roomName) {
|
||||||
|
this.roomName = roomName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoomCode() {
|
||||||
|
return roomCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoomCode(String roomCode) {
|
||||||
|
this.roomCode = roomCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTypeId() {
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeId(Integer typeId) {
|
||||||
|
this.typeId = typeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCapacity() {
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCapacity(Integer capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSeatCount() {
|
||||||
|
return seatCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSeatCount(Integer seatCount) {
|
||||||
|
this.seatCount = seatCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yunhai.acaclassroom.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备关联项(教室批量添加/编辑请求用):设备 id + 每间教室数量。
|
||||||
|
* <p>数量缺省或非法(<1)时按 1 处理,上限 999。</p>
|
||||||
|
*/
|
||||||
|
public class ClassroomEquipmentItem {
|
||||||
|
|
||||||
|
/** 设备 id(aca_equipment.id) */
|
||||||
|
private Integer equipmentId;
|
||||||
|
|
||||||
|
/** 每间教室的设备数量(缺省 1) */
|
||||||
|
private Integer quantity;
|
||||||
|
|
||||||
|
public Integer getEquipmentId() {
|
||||||
|
return equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEquipmentId(Integer equipmentId) {
|
||||||
|
this.equipmentId = equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getQuantity() {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuantity(Integer quantity) {
|
||||||
|
this.quantity = quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Building;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋仓储。
|
||||||
|
*/
|
||||||
|
public interface BuildingRepository extends JpaRepository<Building, Integer> {
|
||||||
|
|
||||||
|
List<Building> findBySchIdOrderBySortOrderAscIdAsc(Integer schId);
|
||||||
|
|
||||||
|
List<Building> findBySchIdAndCampusIdOrderBySortOrderAscIdAsc(Integer schId, Integer campusId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndBuildingCode(Integer schId, String buildingCode);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndBuildingCodeAndIdNot(Integer schId, String buildingCode, Integer id);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndCampusId(Integer schId, Integer campusId);
|
||||||
|
|
||||||
|
long countBySchIdAndCampusId(Integer schId, Integer campusId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Campus;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校区仓储。
|
||||||
|
*/
|
||||||
|
public interface CampusRepository extends JpaRepository<Campus, Integer> {
|
||||||
|
|
||||||
|
List<Campus> findBySchIdOrderBySortOrderAscIdAsc(Integer schId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndCampusName(Integer schId, String campusName);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndCampusNameAndIdNot(Integer schId, String campusName, Integer id);
|
||||||
|
|
||||||
|
long countBySchIdAndCampusName(Integer schId, String campusName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.ClassroomEquipment;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室-设备关联仓储。
|
||||||
|
*/
|
||||||
|
public interface ClassroomEquipmentRepository extends JpaRepository<ClassroomEquipment, Integer> {
|
||||||
|
|
||||||
|
List<ClassroomEquipment> findBySchId(Integer schId);
|
||||||
|
|
||||||
|
List<ClassroomEquipment> findBySchIdAndClassroomId(Integer schId, Integer classroomId);
|
||||||
|
|
||||||
|
List<ClassroomEquipment> findBySchIdAndClassroomIdIn(Integer schId, List<Integer> classroomIds);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndEquipmentId(Integer schId, Integer equipmentId);
|
||||||
|
|
||||||
|
void deleteBySchIdAndClassroomId(Integer schId, Integer classroomId);
|
||||||
|
|
||||||
|
void deleteBySchIdAndClassroomIdIn(Integer schId, List<Integer> classroomIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室仓储。列表/统计数据量小(单校教室百级),按楼栋整取后在服务层过滤分页。
|
||||||
|
*/
|
||||||
|
public interface ClassroomRepository extends JpaRepository<Classroom, Integer> {
|
||||||
|
|
||||||
|
List<Classroom> findBySchIdAndBuildingIdOrderByFloorAscRoomCodeAsc(Integer schId, Integer buildingId);
|
||||||
|
|
||||||
|
List<Classroom> findBySchIdOrderByFloorAscRoomCodeAsc(Integer schId);
|
||||||
|
|
||||||
|
List<Classroom> findBySchId(Integer schId);
|
||||||
|
|
||||||
|
List<Classroom> findBySchIdAndCampusId(Integer schId, Integer campusId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndBuildingId(Integer schId, Integer buildingId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndBuildingIdAndRoomCode(Integer schId, Integer buildingId, String roomCode);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndBuildingIdAndRoomCodeAndIdNot(Integer schId, Integer buildingId, String roomCode, Integer id);
|
||||||
|
|
||||||
|
long countBySchIdAndTypeId(Integer schId, Integer typeId);
|
||||||
|
|
||||||
|
/** 绑定班级占用查询:某班级是否已被本校教室绑定(一个班级同时只绑一间) */
|
||||||
|
boolean existsBySchIdAndClassId(Integer schId, Integer classId);
|
||||||
|
|
||||||
|
List<Classroom> findBySchIdAndClassIdIn(Integer schId, List<Integer> classIds);
|
||||||
|
|
||||||
|
void deleteBySchIdAndIdIn(Integer schId, List<Integer> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.ClassroomType;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室类型仓储。
|
||||||
|
*/
|
||||||
|
public interface ClassroomTypeRepository extends JpaRepository<ClassroomType, Integer> {
|
||||||
|
|
||||||
|
List<ClassroomType> findBySchIdOrderBySortOrderAscIdAsc(Integer schId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndTypeName(Integer schId, String typeName);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndTypeNameAndIdNot(Integer schId, String typeName, Integer id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Equipment;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室设备仓储(独立设备台账)。
|
||||||
|
*/
|
||||||
|
public interface EquipmentRepository extends JpaRepository<Equipment, Integer> {
|
||||||
|
|
||||||
|
List<Equipment> findBySchId(Integer schId);
|
||||||
|
|
||||||
|
List<Equipment> findBySchIdOrderByIdDesc(Integer schId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndEquipmentName(Integer schId, String equipmentName);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndEquipmentNameAndIdNot(Integer schId, String equipmentName, Integer id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.SchoolClass;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级仓储。单校班级量级为十~百级,整取后服务层过滤。
|
||||||
|
*/
|
||||||
|
public interface SchoolClassRepository extends JpaRepository<SchoolClass, Integer> {
|
||||||
|
|
||||||
|
List<SchoolClass> findBySchIdOrderByGradeAscIdAsc(Integer schId);
|
||||||
|
|
||||||
|
boolean existsBySchIdAndClassName(Integer schId, String className);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yunhai.acaclassroom.repository;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.SysOperationLog;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志仓储。
|
||||||
|
*/
|
||||||
|
public interface SysOperationLogRepository extends JpaRepository<SysOperationLog, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋管理服务。
|
||||||
|
*/
|
||||||
|
public interface IBuildingService {
|
||||||
|
|
||||||
|
/** 楼栋列表(campusId 为空返回全部,含所属校区名与教室数)。 */
|
||||||
|
List<Map<String, Object>> list(Integer campusId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋统计(教室管理页"显示图":每楼栋教室 开放/关闭/总数)。
|
||||||
|
* 每项额外含 floors 楼层分布([{floor,total,open,closed}],楼层升序,仅实际有教室的楼层),
|
||||||
|
* 供前端楼栋卡片绘制楼层条;为新增字段,旧调用方忽略即可。
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> stats(Integer campusId);
|
||||||
|
|
||||||
|
/** 新增楼栋,返回新 id。 */
|
||||||
|
Integer add(Integer campusId, String buildingName, String buildingCode, Integer floorCount, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 编辑楼栋(楼层调小时不校验已有教室楼层,由前端提示)。 */
|
||||||
|
void edit(Integer serID, Integer campusId, String buildingName, String buildingCode, Integer floorCount, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 删除楼栋(存在教室时拒绝)。 */
|
||||||
|
void delete(Integer serid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校区管理服务。
|
||||||
|
*/
|
||||||
|
public interface ICampusService {
|
||||||
|
|
||||||
|
/** 校区列表(含楼栋数/教室数,管理页用)。 */
|
||||||
|
List<Map<String, Object>> list();
|
||||||
|
|
||||||
|
/** 校区统计(总览页:每校区楼栋数与教室 开放/关闭/总数)。 */
|
||||||
|
List<Map<String, Object>> stats();
|
||||||
|
|
||||||
|
/** 新增校区,返回新 id。 */
|
||||||
|
Integer add(String campusName, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 编辑校区。 */
|
||||||
|
void edit(Integer serID, String campusName, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 删除校区(存在楼栋时拒绝)。 */
|
||||||
|
void delete(Integer serid);
|
||||||
|
|
||||||
|
/** 按 ids 顺序重排(sortOrder = 序号)。 */
|
||||||
|
void sort(List<Integer> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级档案服务(绑定班级页)。
|
||||||
|
*/
|
||||||
|
public interface IClassService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级列表(全校,可按年级过滤)。
|
||||||
|
*
|
||||||
|
* @param grade 1=高一 2=高二 3=高三(可空=全部)
|
||||||
|
* @return 行含 serID/className/grade/studentCount/headTeacher/classroomId(未绑定为 null)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> list(Integer grade);
|
||||||
|
|
||||||
|
/** 新增班级,返回 id。 */
|
||||||
|
Integer add(String className, Integer grade, Integer studentCount, String headTeacher);
|
||||||
|
|
||||||
|
/** 删除班级(已被教室绑定时拒绝)。 */
|
||||||
|
void delete(Integer id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchAddRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchEditRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomEditRequest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室管理服务。
|
||||||
|
*/
|
||||||
|
public interface IClassroomService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室列表(空间作用域:按楼栋或按校区,可按楼层/类型/状态/关键字过滤,内存分页)。
|
||||||
|
*
|
||||||
|
* @param buildingId 楼栋 id(与 campusId 至少传其一,楼栋优先)
|
||||||
|
* @param campusId 校区 id(buildingId 为空时按校区查全部楼栋的教室)
|
||||||
|
* @param floor 楼层(可空)
|
||||||
|
* @param typeId 教室类型(可空)
|
||||||
|
* @param status open/closed(可空)
|
||||||
|
* @param keyword 教室名称/编号模糊(可空)
|
||||||
|
* @param grade 年级过滤(绑定班级页 Tab:1=高一 2=高二 3=高三;可空不过滤。
|
||||||
|
* 过滤口径=未绑定教室 + 绑定班级为该年级的教室)
|
||||||
|
* @param bindStatus 绑定状态过滤 bound/unbound(可空)
|
||||||
|
* @param page 页码(1 起)
|
||||||
|
* @param count 每页条数
|
||||||
|
* @param sortField 排序字段 floor/roomCode/roomName/status/capacity/seatCount(可空=默认序:楼层升序+编号升序)
|
||||||
|
* @param order asc/desc(可空=asc)
|
||||||
|
*/
|
||||||
|
Map<String, Object> list(Integer buildingId, Integer campusId, Integer floor, Integer typeId, String status,
|
||||||
|
String keyword, Integer grade, String bindStatus, Integer page, Integer count,
|
||||||
|
String sortField, String order);
|
||||||
|
|
||||||
|
/** 楼栋下已有教室的楼层列表(升序,筛选项用)。 */
|
||||||
|
List<Integer> floors(Integer buildingId);
|
||||||
|
|
||||||
|
/** 全校教室扁平列表(空间筛选树数据源,仅轻量字段)。 */
|
||||||
|
List<Map<String, Object>> tree();
|
||||||
|
|
||||||
|
/** 批量添加教室,返回 {created, skipped, total}。 */
|
||||||
|
Map<String, Object> batchAdd(ClassroomBatchAddRequest req);
|
||||||
|
|
||||||
|
/** 编辑教室(类型/名称/编号/状态/容纳人数/座位数)。 */
|
||||||
|
void edit(ClassroomEditRequest req);
|
||||||
|
|
||||||
|
/** 批量编辑教室(仅状态 / 教室类型,可只改其一),返回 {updated, skipped, total}。 */
|
||||||
|
Map<String, Object> batchEdit(ClassroomBatchEditRequest req);
|
||||||
|
|
||||||
|
/** 批量删除教室。 */
|
||||||
|
void delete(List<Integer> ids);
|
||||||
|
|
||||||
|
/** 教室绑定班级(班级同校且未被其他教室占用),返回 {classId, className}。 */
|
||||||
|
Map<String, Object> bindClass(Integer serID, Integer classId);
|
||||||
|
|
||||||
|
/** 教室解绑班级(未绑定时静默成功)。 */
|
||||||
|
void unbindClass(Integer serID);
|
||||||
|
|
||||||
|
/** 批量解绑(未绑定/不存在的教室计入 skipped),返回 {updated, skipped, total}。 */
|
||||||
|
Map<String, Object> batchUnbind(List<Integer> serIDs);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室类型管理服务。
|
||||||
|
*/
|
||||||
|
public interface IClassroomTypeService {
|
||||||
|
|
||||||
|
/** 类型列表(含引用的教室数)。 */
|
||||||
|
List<Map<String, Object>> list();
|
||||||
|
|
||||||
|
/** 新增类型,返回新 id。 */
|
||||||
|
Integer add(String typeName, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 编辑类型(名称/启停/排序)。 */
|
||||||
|
void edit(Integer serID, String typeName, Integer enableFlag, Integer sortOrder);
|
||||||
|
|
||||||
|
/** 删除类型(已被教室引用时拒绝)。 */
|
||||||
|
void delete(Integer serid);
|
||||||
|
|
||||||
|
/** 按 ids 顺序重排。 */
|
||||||
|
void sort(List<Integer> ids);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室设备管理服务(基础设置 · 设备管理)。独立设备台账,不挂教室;
|
||||||
|
* 教室(批量添加/编辑)通过教室-设备关联引用设备。
|
||||||
|
*/
|
||||||
|
public interface IEquipmentService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备列表(全校全量,可按关键字过滤)。
|
||||||
|
* 行含 quantity(按教室关联用量汇总的使用数量)与 classroomCount(引用该设备的教室数)。
|
||||||
|
*/
|
||||||
|
Map<String, Object> list(String keyword);
|
||||||
|
|
||||||
|
/** 新增设备(名称校内唯一),返回新 id。 */
|
||||||
|
Integer add(String equipmentName, String remark);
|
||||||
|
|
||||||
|
/** 编辑设备(名称/备注)。 */
|
||||||
|
void edit(Integer serID, String equipmentName, String remark);
|
||||||
|
|
||||||
|
/** 删除设备(被教室引用时拒绝)。 */
|
||||||
|
void delete(Integer serid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.SysOperationLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志服务(审计轨)。落库走专用线程池异步执行,失败仅告警。
|
||||||
|
*/
|
||||||
|
public interface ISysOperationLogService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步保存一条操作日志(auditLogExecutor 线程池)。
|
||||||
|
* 绝不抛出影响主业务的异常。
|
||||||
|
*/
|
||||||
|
void saveAsync(SysOperationLog entity);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.yunhai.acaclassroom.service;
|
||||||
|
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。
|
||||||
|
* 封装 {@code YschBaseOpenApi} 静态门面调用,Controller 只保留"取参 → 调 Service → 返回"的薄壳。
|
||||||
|
*/
|
||||||
|
public interface IUserCenterService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户可访问的模块树(左侧菜单)。
|
||||||
|
* <p>从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构
|
||||||
|
* ({@code modId/page/children}),对应前端 GetUserMod 接口。</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param schId 学校ID(可为 null)
|
||||||
|
* @return 模块树(顶层 List,含 children 嵌套)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getUserModuleTree(Integer userId, Integer schId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析"当前用户":CAS 会话身份优先原样返回;无会话且 dev-context 开启时,
|
||||||
|
* 按 {@code acaclassroom.dev-context.login-name} 查平台用户中心返回真实用户
|
||||||
|
* (含姓名/头像,供顶栏展示与 UserContext 审计身份使用)。
|
||||||
|
* <p>查无此人/平台不可达/未配置 login-name 时返回 null,由调用方按各自语义兜底
|
||||||
|
* (GetUserInfo 返回空,切面回退占位身份 99999)。</p>
|
||||||
|
*
|
||||||
|
* @param casUser CAS 会话注入的用户(可为 null)
|
||||||
|
* @return 当前用户;无法确定时为 null
|
||||||
|
*/
|
||||||
|
BoaUser getCurrentUser(BoaUser casUser);
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Building;
|
||||||
|
import com.yunhai.acaclassroom.entity.Campus;
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import com.yunhai.acaclassroom.repository.BuildingRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.CampusRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.IBuildingService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼栋管理服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class BuildingService implements IBuildingService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BuildingRepository buildingRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CampusRepository campusRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomRepository classroomRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> list(Integer campusId) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Building> buildings = campusId != null
|
||||||
|
? buildingRepository.findBySchIdAndCampusIdOrderBySortOrderAscIdAsc(schId, campusId)
|
||||||
|
: buildingRepository.findBySchIdOrderBySortOrderAscIdAsc(schId);
|
||||||
|
Map<Integer, String> campusNames = campusRepository.findBySchIdOrderBySortOrderAscIdAsc(schId).stream()
|
||||||
|
.collect(Collectors.toMap(Campus::getId, Campus::getCampusName, (a, b) -> a, LinkedHashMap::new));
|
||||||
|
Map<Integer, Long> roomCount = classroomRepository.findBySchId(schId).stream()
|
||||||
|
.collect(Collectors.groupingBy(Classroom::getBuildingId, Collectors.counting()));
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Building b : buildings) {
|
||||||
|
Map<String, Object> vo = toVo(b);
|
||||||
|
vo.put("campusName", campusNames.getOrDefault(b.getCampusId(), ""));
|
||||||
|
vo.put("classroomCount", roomCount.getOrDefault(b.getId(), 0L));
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> stats(Integer campusId) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Building> buildings = campusId != null
|
||||||
|
? buildingRepository.findBySchIdAndCampusIdOrderBySortOrderAscIdAsc(schId, campusId)
|
||||||
|
: buildingRepository.findBySchIdOrderBySortOrderAscIdAsc(schId);
|
||||||
|
// 一次取全校教室,按楼栋聚合(避免逐楼栋查库)
|
||||||
|
Map<Integer, List<Classroom>> byBuilding = classroomRepository.findBySchId(schId).stream()
|
||||||
|
.collect(Collectors.groupingBy(Classroom::getBuildingId));
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Building b : buildings) {
|
||||||
|
List<Classroom> rooms = byBuilding.getOrDefault(b.getId(), Collections.emptyList());
|
||||||
|
long open = rooms.stream().filter(r -> "open".equals(r.getStatus())).count();
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", b.getId());
|
||||||
|
vo.put("campusId", b.getCampusId());
|
||||||
|
vo.put("buildingName", b.getBuildingName());
|
||||||
|
vo.put("buildingCode", b.getBuildingCode());
|
||||||
|
vo.put("floorCount", b.getFloorCount());
|
||||||
|
vo.put("sortOrder", b.getSortOrder());
|
||||||
|
vo.put("total", rooms.size());
|
||||||
|
vo.put("open", open);
|
||||||
|
vo.put("closed", rooms.size() - open);
|
||||||
|
// 楼层分布(新增字段,供教室管理页楼栋卡片绘制「楼层条」;仅含实际有教室的楼层,按楼层升序)
|
||||||
|
vo.put("floors", floorBreakdown(rooms));
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按楼层聚合教室数(total/open/closed),楼层升序。 */
|
||||||
|
private List<Map<String, Object>> floorBreakdown(List<Classroom> rooms) {
|
||||||
|
Map<Integer, List<Classroom>> byFloor = rooms.stream()
|
||||||
|
.filter(r -> r.getFloor() != null)
|
||||||
|
.collect(Collectors.groupingBy(Classroom::getFloor, TreeMap::new, Collectors.toList()));
|
||||||
|
List<Map<String, Object>> floors = new ArrayList<>();
|
||||||
|
for (Map.Entry<Integer, List<Classroom>> e : byFloor.entrySet()) {
|
||||||
|
List<Classroom> list = e.getValue();
|
||||||
|
long fOpen = list.stream().filter(r -> "open".equals(r.getStatus())).count();
|
||||||
|
Map<String, Object> f = new LinkedHashMap<>();
|
||||||
|
f.put("floor", e.getKey());
|
||||||
|
f.put("total", list.size());
|
||||||
|
f.put("open", fOpen);
|
||||||
|
f.put("closed", list.size() - fOpen);
|
||||||
|
floors.add(f);
|
||||||
|
}
|
||||||
|
return floors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Integer add(Integer campusId, String buildingName, String buildingCode, Integer floorCount, Integer sortOrder) {
|
||||||
|
String name = requireText(buildingName, "楼栋名称不得为空");
|
||||||
|
String code = requireText(buildingCode, "楼栋编号不得为空");
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
Campus campus = loadOwnedCampus(campusId);
|
||||||
|
if (buildingRepository.existsBySchIdAndBuildingCode(schId, code)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋编号已存在");
|
||||||
|
}
|
||||||
|
Building b = new Building();
|
||||||
|
b.setCampusId(campus.getId());
|
||||||
|
b.setBuildingName(name);
|
||||||
|
b.setBuildingCode(code);
|
||||||
|
b.setFloorCount(requireFloorCount(floorCount));
|
||||||
|
b.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
b.setSchId(schId);
|
||||||
|
return buildingRepository.save(b).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void edit(Integer serID, Integer campusId, String buildingName, String buildingCode, Integer floorCount, Integer sortOrder) {
|
||||||
|
String name = requireText(buildingName, "楼栋名称不得为空");
|
||||||
|
String code = requireText(buildingCode, "楼栋编号不得为空");
|
||||||
|
Building b = loadOwned(serID);
|
||||||
|
if (buildingRepository.existsBySchIdAndBuildingCodeAndIdNot(b.getSchId(), code, serID)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋编号已存在");
|
||||||
|
}
|
||||||
|
Integer oldCampusId = b.getCampusId();
|
||||||
|
if (campusId != null) {
|
||||||
|
Campus campus = loadOwnedCampus(campusId);
|
||||||
|
b.setCampusId(campus.getId());
|
||||||
|
}
|
||||||
|
b.setBuildingName(name);
|
||||||
|
b.setBuildingCode(code);
|
||||||
|
b.setFloorCount(requireFloorCount(floorCount));
|
||||||
|
b.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
buildingRepository.save(b);
|
||||||
|
// 楼栋归属变化时同步教室冗余校区
|
||||||
|
if (campusId != null && !campusId.equals(oldCampusId)) {
|
||||||
|
for (Classroom r : classroomRepository.findBySchIdAndBuildingIdOrderByFloorAscRoomCodeAsc(b.getSchId(), serID)) {
|
||||||
|
r.setCampusId(campusId);
|
||||||
|
classroomRepository.save(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer serid) {
|
||||||
|
Building b = loadOwned(serid);
|
||||||
|
if (classroomRepository.existsBySchIdAndBuildingId(b.getSchId(), b.getId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该楼栋下存在教室,请先删除教室");
|
||||||
|
}
|
||||||
|
buildingRepository.delete(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toVo(Building b) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", b.getId());
|
||||||
|
vo.put("campusId", b.getCampusId());
|
||||||
|
vo.put("buildingName", b.getBuildingName());
|
||||||
|
vo.put("buildingCode", b.getBuildingCode());
|
||||||
|
vo.put("floorCount", b.getFloorCount());
|
||||||
|
vo.put("sortOrder", b.getSortOrder());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载本校楼栋(跨校/不存在报错)。 */
|
||||||
|
Building loadOwned(Integer id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋ID不得为空");
|
||||||
|
}
|
||||||
|
Building b = buildingRepository.findById(id).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (b == null || schId == null || !schId.equals(b.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋不存在或无权操作");
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Campus loadOwnedCampus(Integer campusId) {
|
||||||
|
if (campusId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "所属校区不得为空");
|
||||||
|
}
|
||||||
|
Campus c = campusRepository.findById(campusId).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (c == null || schId == null || !schId.equals(c.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "所属校区不存在");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireFloorCount(Integer floorCount) {
|
||||||
|
if (floorCount == null || floorCount < 1 || floorCount > 99) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼层数量必须在 1~99 之间");
|
||||||
|
}
|
||||||
|
return floorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireSchId() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireText(String s, String message) {
|
||||||
|
if (s == null || s.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, message);
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Campus;
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import com.yunhai.acaclassroom.repository.BuildingRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.CampusRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.ICampusService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校区管理服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CampusService implements ICampusService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CampusRepository campusRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BuildingRepository buildingRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomRepository classroomRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> list() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Campus> campuses = campusRepository.findBySchIdOrderBySortOrderAscIdAsc(schId);
|
||||||
|
// 楼栋数按校区聚合;教室数用冗余 campus_id 直查
|
||||||
|
Map<Integer, Long> buildingCount = new HashMap<>();
|
||||||
|
for (Campus c : campuses) {
|
||||||
|
buildingCount.put(c.getId(), buildingRepository.countBySchIdAndCampusId(schId, c.getId()));
|
||||||
|
}
|
||||||
|
Map<Integer, Long> classroomCount = classroomRepository.findBySchId(schId).stream()
|
||||||
|
.collect(Collectors.groupingBy(Classroom::getCampusId, Collectors.counting()));
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Campus c : campuses) {
|
||||||
|
Map<String, Object> vo = toVo(c);
|
||||||
|
vo.put("buildingCount", buildingCount.getOrDefault(c.getId(), 0L));
|
||||||
|
vo.put("classroomCount", classroomCount.getOrDefault(c.getId(), 0L));
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> stats() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Campus c : campusRepository.findBySchIdOrderBySortOrderAscIdAsc(schId)) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", c.getId());
|
||||||
|
vo.put("campusName", c.getCampusName());
|
||||||
|
vo.put("buildingCount", buildingRepository.countBySchIdAndCampusId(schId, c.getId()));
|
||||||
|
long total = 0;
|
||||||
|
long open = 0;
|
||||||
|
for (Classroom r : classroomRepository.findBySchIdAndCampusId(schId, c.getId())) {
|
||||||
|
total++;
|
||||||
|
if ("open".equals(r.getStatus())) {
|
||||||
|
open++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vo.put("total", total);
|
||||||
|
vo.put("open", open);
|
||||||
|
vo.put("closed", total - open);
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Integer add(String campusName, Integer sortOrder) {
|
||||||
|
String name = requireText(campusName, "校区名称不得为空");
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
if (campusRepository.existsBySchIdAndCampusName(schId, name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "校区名称已存在");
|
||||||
|
}
|
||||||
|
Campus c = new Campus();
|
||||||
|
c.setCampusName(name);
|
||||||
|
c.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
c.setSchId(schId);
|
||||||
|
return campusRepository.save(c).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void edit(Integer serID, String campusName, Integer sortOrder) {
|
||||||
|
String name = requireText(campusName, "校区名称不得为空");
|
||||||
|
Campus c = loadOwned(serID);
|
||||||
|
if (campusRepository.existsBySchIdAndCampusNameAndIdNot(c.getSchId(), name, serID)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "校区名称已存在");
|
||||||
|
}
|
||||||
|
c.setCampusName(name);
|
||||||
|
c.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
campusRepository.save(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer serid) {
|
||||||
|
Campus c = loadOwned(serid);
|
||||||
|
if (buildingRepository.existsBySchIdAndCampusId(c.getSchId(), c.getId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该校区下存在楼栋,请先删除楼栋");
|
||||||
|
}
|
||||||
|
campusRepository.delete(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void sort(List<Integer> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ids.size(); i++) {
|
||||||
|
Campus c = loadOwned(ids.get(i));
|
||||||
|
c.setSortOrder(i + 1);
|
||||||
|
campusRepository.save(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toVo(Campus c) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", c.getId());
|
||||||
|
vo.put("campusName", c.getCampusName());
|
||||||
|
vo.put("sortOrder", c.getSortOrder());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载本校校区(跨校/不存在报错)。 */
|
||||||
|
private Campus loadOwned(Integer id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "校区ID不得为空");
|
||||||
|
}
|
||||||
|
Campus c = campusRepository.findById(id).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (c == null || schId == null || !schId.equals(c.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "校区不存在或无权操作");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireSchId() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireText(String s, String message) {
|
||||||
|
if (s == null || s.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, message);
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import com.yunhai.acaclassroom.entity.SchoolClass;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.SchoolClassRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.IClassService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级档案服务实现。班级数据量小(单校十~百级),整取后内存过滤。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClassService implements IClassService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SchoolClassRepository classRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomRepository classroomRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> list(Integer grade) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<SchoolClass> classes = classRepository.findBySchIdOrderByGradeAscIdAsc(schId);
|
||||||
|
if (grade != null) {
|
||||||
|
classes = classes.stream().filter(c -> grade.equals(c.getGrade())).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
// 班级 → 绑定的教室 id(绑定状态下拉过滤/行回显用)
|
||||||
|
List<Integer> ids = classes.stream().map(SchoolClass::getId).collect(Collectors.toList());
|
||||||
|
Map<Integer, Integer> boundRooms = ids.isEmpty()
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: classroomRepository.findBySchIdAndClassIdIn(schId, ids).stream()
|
||||||
|
.collect(Collectors.toMap(Classroom::getClassId, Classroom::getId, (a, b) -> a));
|
||||||
|
return classes.stream().map(c -> {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", c.getId());
|
||||||
|
vo.put("className", c.getClassName());
|
||||||
|
vo.put("grade", c.getGrade());
|
||||||
|
vo.put("studentCount", c.getStudentCount());
|
||||||
|
vo.put("headTeacher", c.getHeadTeacher());
|
||||||
|
vo.put("classroomId", boundRooms.get(c.getId()));
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Integer add(String className, Integer grade, Integer studentCount, String headTeacher) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
String name = className != null ? className.trim() : "";
|
||||||
|
if (name.isEmpty() || name.length() > 50) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请输入 50 字以内的班级名称");
|
||||||
|
}
|
||||||
|
if (grade == null || grade < SchoolClass.GRADE_1 || grade > SchoolClass.GRADE_3) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "年级只支持 1=高一 2=高二 3=高三");
|
||||||
|
}
|
||||||
|
if (classRepository.existsBySchIdAndClassName(schId, name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "班级名称已存在");
|
||||||
|
}
|
||||||
|
int count = studentCount == null ? 0 : studentCount;
|
||||||
|
if (count < 0 || count > 10000) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "学生人数不正确");
|
||||||
|
}
|
||||||
|
SchoolClass c = new SchoolClass();
|
||||||
|
c.setClassName(name);
|
||||||
|
c.setGrade(grade);
|
||||||
|
c.setStudentCount(count);
|
||||||
|
c.setHeadTeacher(headTeacher != null && !headTeacher.trim().isEmpty() ? headTeacher.trim() : null);
|
||||||
|
c.setSchId(schId);
|
||||||
|
return classRepository.save(c).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer id) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
SchoolClass c = classRepository.findById(id).orElse(null);
|
||||||
|
if (c == null || !schId.equals(c.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "班级不存在或无权操作");
|
||||||
|
}
|
||||||
|
if (classroomRepository.existsBySchIdAndClassId(schId, id)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该班级已绑定教室,请先解绑再删除");
|
||||||
|
}
|
||||||
|
classRepository.delete(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,610 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Building;
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import com.yunhai.acaclassroom.entity.ClassroomType;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchAddRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomBatchEditRequest;
|
||||||
|
import com.yunhai.acaclassroom.model.ClassroomEditRequest;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomTypeRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.EquipmentRepository;import com.yunhai.acaclassroom.service.IClassroomService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室管理服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClassroomService implements IClassroomService {
|
||||||
|
|
||||||
|
/** 状态常量:开放 */
|
||||||
|
public static final String STATUS_OPEN = "open";
|
||||||
|
/** 状态常量:关闭 */
|
||||||
|
public static final String STATUS_CLOSED = "closed";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomRepository classroomRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomTypeRepository typeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EquipmentRepository equipmentRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yunhai.acaclassroom.repository.ClassroomEquipmentRepository classroomEquipmentRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yunhai.acaclassroom.repository.CampusRepository campusRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yunhai.acaclassroom.repository.BuildingRepository buildingRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.yunhai.acaclassroom.repository.SchoolClassRepository classRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BuildingService buildingService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> list(Integer buildingId, Integer campusId, Integer floor, Integer typeId, String status,
|
||||||
|
String keyword, Integer grade, String bindStatus,
|
||||||
|
Integer page, Integer count, String sortField, String order) {
|
||||||
|
if (buildingId == null && campusId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请至少指定校区或楼栋");
|
||||||
|
}
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
Map<String, Object> empty = new LinkedHashMap<>();
|
||||||
|
empty.put("list", Collections.emptyList());
|
||||||
|
empty.put("total", 0);
|
||||||
|
empty.put("pageIndex", 1);
|
||||||
|
empty.put("pageSize", 0);
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
// 作用域:楼栋优先;无楼栋时按校区取全部楼栋的教室
|
||||||
|
List<Classroom> rooms = buildingId != null
|
||||||
|
? classroomRepository.findBySchIdAndBuildingIdOrderByFloorAscRoomCodeAsc(schId, buildingId)
|
||||||
|
: classroomRepository.findBySchIdAndCampusId(schId, campusId);
|
||||||
|
|
||||||
|
// 过滤(数据量小,内存过滤)
|
||||||
|
if (floor != null) {
|
||||||
|
rooms = rooms.stream().filter(r -> floor.equals(r.getFloor())).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (typeId != null) {
|
||||||
|
rooms = rooms.stream().filter(r -> typeId.equals(r.getTypeId())).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (status != null && !status.isEmpty()) {
|
||||||
|
rooms = rooms.stream().filter(r -> status.equals(r.getStatus())).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||||
|
String kw = keyword.trim().toLowerCase(Locale.ROOT);
|
||||||
|
rooms = rooms.stream().filter(r ->
|
||||||
|
(r.getRoomName() != null && r.getRoomName().toLowerCase(Locale.ROOT).contains(kw))
|
||||||
|
|| (r.getRoomCode() != null && r.getRoomCode().toLowerCase(Locale.ROOT).contains(kw)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 班级映射:绑定状态过滤 / 年级 Tab 过滤 / 行内班级信息展示共用
|
||||||
|
Map<Integer, com.yunhai.acaclassroom.entity.SchoolClass> classMap = classRepository
|
||||||
|
.findBySchIdOrderByGradeAscIdAsc(schId).stream()
|
||||||
|
.collect(Collectors.toMap(com.yunhai.acaclassroom.entity.SchoolClass::getId, c -> c, (a, b) -> a));
|
||||||
|
if ("bound".equals(bindStatus)) {
|
||||||
|
rooms = rooms.stream().filter(r -> r.getClassId() != null).collect(Collectors.toList());
|
||||||
|
} else if ("unbound".equals(bindStatus)) {
|
||||||
|
rooms = rooms.stream().filter(r -> r.getClassId() == null).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
if (grade != null) {
|
||||||
|
// Tab 口径:未绑定的教室(当前年级可绑)+ 已绑定该年级班级的教室
|
||||||
|
rooms = rooms.stream().filter(r -> {
|
||||||
|
if (r.getClassId() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
com.yunhai.acaclassroom.entity.SchoolClass c = classMap.get(r.getClassId());
|
||||||
|
return c != null && grade.equals(c.getGrade());
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校区作用域取数无序,先补齐默认序(楼层升序 + 编号升序),与楼栋作用域的仓储序一致
|
||||||
|
if (buildingId == null) {
|
||||||
|
rooms.sort(Comparator.comparing(Classroom::getFloor,
|
||||||
|
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||||
|
.thenComparing(Classroom::getRoomCode, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序(sortField 为空时保持仓储层默认序:楼层升序 + 编号升序,与改造前行为一致)
|
||||||
|
rooms = applySort(rooms, sortField, order);
|
||||||
|
|
||||||
|
// 类型名映射
|
||||||
|
Map<Integer, String> typeNames = typeRepository.findBySchIdOrderBySortOrderAscIdAsc(schId).stream()
|
||||||
|
.collect(Collectors.toMap(ClassroomType::getId, ClassroomType::getTypeName, (a, b) -> a));
|
||||||
|
// 校区/楼栋名映射(列表跨楼栋时行内直接展示,空间筛选树与详情面板用)
|
||||||
|
Map<Integer, String> campusNames = campusRepository.findBySchIdOrderBySortOrderAscIdAsc(schId).stream()
|
||||||
|
.collect(Collectors.toMap(com.yunhai.acaclassroom.entity.Campus::getId,
|
||||||
|
com.yunhai.acaclassroom.entity.Campus::getCampusName, (a, b) -> a));
|
||||||
|
Map<Integer, String> buildingNames = buildingRepository.findBySchIdOrderBySortOrderAscIdAsc(schId).stream()
|
||||||
|
.collect(Collectors.toMap(Building::getId, Building::getBuildingName, (a, b) -> a));
|
||||||
|
|
||||||
|
// 分页(1 起)
|
||||||
|
int pageIndex = page != null && page >= 1 ? page : 1;
|
||||||
|
int pageSize = count != null && count >= 1 ? Math.min(count, 200) : 20;
|
||||||
|
int total = rooms.size();
|
||||||
|
int from = Math.min((pageIndex - 1) * pageSize, total);
|
||||||
|
int to = Math.min(from + pageSize, total);
|
||||||
|
|
||||||
|
List<Classroom> pageRooms = rooms.subList(from, to);
|
||||||
|
|
||||||
|
// 绑定班级信息(行内展示:班级名/学生人数/班主任)
|
||||||
|
// 本页教室的设备关联 → 设备名映射、数量映射(quantity 列为后加,存量 NULL 按 1)
|
||||||
|
List<Integer> pageRoomIds = pageRooms.stream().map(Classroom::getId).collect(Collectors.toList());
|
||||||
|
Map<Integer, Map<Integer, Integer>> roomEquipmentQty = new HashMap<>();
|
||||||
|
Map<Integer, String> equipmentNames = new HashMap<>();
|
||||||
|
if (!pageRoomIds.isEmpty()) {
|
||||||
|
equipmentRepository.findBySchId(schId).forEach(eq ->
|
||||||
|
equipmentNames.put(eq.getId(), eq.getEquipmentName()));
|
||||||
|
for (com.yunhai.acaclassroom.entity.ClassroomEquipment ce :
|
||||||
|
classroomEquipmentRepository.findBySchIdAndClassroomIdIn(schId, pageRoomIds)) {
|
||||||
|
int qty = ce.getQuantity() == null || ce.getQuantity() < 1 ? 1 : ce.getQuantity();
|
||||||
|
roomEquipmentQty.computeIfAbsent(ce.getClassroomId(), k -> new LinkedHashMap<>())
|
||||||
|
.put(ce.getEquipmentId(), qty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
for (Classroom r : pageRooms) {
|
||||||
|
Map<String, Object> vo = toVo(r, typeNames);
|
||||||
|
vo.put("campusName", campusNames.getOrDefault(r.getCampusId(), ""));
|
||||||
|
vo.put("buildingName", buildingNames.getOrDefault(r.getBuildingId(), ""));
|
||||||
|
com.yunhai.acaclassroom.entity.SchoolClass bound = r.getClassId() != null ? classMap.get(r.getClassId()) : null;
|
||||||
|
vo.put("classId", r.getClassId());
|
||||||
|
vo.put("className", bound != null ? bound.getClassName() : null);
|
||||||
|
vo.put("studentCount", bound != null ? bound.getStudentCount() : null);
|
||||||
|
vo.put("headTeacher", bound != null ? bound.getHeadTeacher() : null);
|
||||||
|
Map<Integer, Integer> eqQty = roomEquipmentQty.getOrDefault(r.getId(), Collections.emptyMap());
|
||||||
|
vo.put("equipmentIds", new ArrayList<>(eqQty.keySet()));
|
||||||
|
vo.put("equipmentQtys", eqQty);
|
||||||
|
vo.put("equipmentNames", eqQty.entrySet().stream()
|
||||||
|
.map(e -> {
|
||||||
|
String name = equipmentNames.getOrDefault(e.getKey(), "");
|
||||||
|
return name.isEmpty() ? "" : (e.getValue() > 1 ? name + "×" + e.getValue() : name);
|
||||||
|
})
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.joining("、")));
|
||||||
|
list.add(vo);
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", list);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("pageIndex", pageIndex);
|
||||||
|
result.put("pageSize", pageSize);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> floors(Integer buildingId) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null || buildingId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return classroomRepository.findBySchIdAndBuildingIdOrderByFloorAscRoomCodeAsc(schId, buildingId).stream()
|
||||||
|
.map(Classroom::getFloor)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> tree() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 单校教室百级,一次整取;前端按 校区→楼栋→楼层 自行组树
|
||||||
|
return classroomRepository.findBySchIdOrderByFloorAscRoomCodeAsc(schId).stream()
|
||||||
|
.map(r -> {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", r.getId());
|
||||||
|
vo.put("campusId", r.getCampusId());
|
||||||
|
vo.put("buildingId", r.getBuildingId());
|
||||||
|
vo.put("floor", r.getFloor());
|
||||||
|
vo.put("roomName", r.getRoomName());
|
||||||
|
vo.put("roomCode", r.getRoomCode());
|
||||||
|
vo.put("status", r.getStatus());
|
||||||
|
return vo;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> batchAdd(ClassroomBatchAddRequest req) {
|
||||||
|
if (req == null || req.getBuildingId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋ID不得为空");
|
||||||
|
}
|
||||||
|
Integer floorFrom = req.getFloorFrom();
|
||||||
|
Integer floorTo = req.getFloorTo();
|
||||||
|
Integer countPerFloor = req.getCountPerFloor();
|
||||||
|
if (floorFrom == null || floorTo == null || floorFrom < 1 || floorTo < floorFrom) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼层区间不正确(需 1 ≤ 起始 ≤ 截止)");
|
||||||
|
}
|
||||||
|
if (countPerFloor == null || countPerFloor < 1 || countPerFloor > 99) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "每层教室数量必须在 1~99 之间");
|
||||||
|
}
|
||||||
|
Building building = buildingService.loadOwned(req.getBuildingId());
|
||||||
|
if (floorTo > building.getFloorCount()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"截止楼层不得超过该楼栋楼层数量(" + building.getFloorCount() + " 层)");
|
||||||
|
}
|
||||||
|
String prefix = req.getNamePrefix() != null && !req.getNamePrefix().trim().isEmpty()
|
||||||
|
? req.getNamePrefix().trim() : building.getBuildingCode();
|
||||||
|
|
||||||
|
// 校验设备 id 均属本校(批量添加:设备按数量应用到本次新建的教室)
|
||||||
|
Map<Integer, Integer> equipmentMap = sanitizeEquipmentMap(req.getEquipments(), req.getEquipmentIds(), building.getSchId());
|
||||||
|
|
||||||
|
int created = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
List<Classroom> createdRooms = new ArrayList<>();
|
||||||
|
for (int floor = floorFrom; floor <= floorTo; floor++) {
|
||||||
|
for (int seq = 1; seq <= countPerFloor; seq++) {
|
||||||
|
String code = String.format("%s%02d%02d", prefix, floor, seq);
|
||||||
|
if (classroomRepository.existsBySchIdAndBuildingIdAndRoomCode(
|
||||||
|
building.getSchId(), building.getId(), code)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Classroom r = new Classroom();
|
||||||
|
r.setCampusId(building.getCampusId());
|
||||||
|
r.setBuildingId(building.getId());
|
||||||
|
r.setFloor(floor);
|
||||||
|
r.setRoomName(code);
|
||||||
|
r.setRoomCode(code);
|
||||||
|
r.setTypeId(req.getTypeId());
|
||||||
|
r.setStatus(STATUS_OPEN);
|
||||||
|
r.setCapacity(0);
|
||||||
|
r.setSeatCount(0);
|
||||||
|
r.setSchId(building.getSchId());
|
||||||
|
createdRooms.add(classroomRepository.save(r));
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 新建教室写入设备关联(含每间数量)
|
||||||
|
if (!equipmentMap.isEmpty()) {
|
||||||
|
for (Classroom r : createdRooms) {
|
||||||
|
equipmentMap.forEach((eqId, qty) -> {
|
||||||
|
com.yunhai.acaclassroom.entity.ClassroomEquipment ce =
|
||||||
|
new com.yunhai.acaclassroom.entity.ClassroomEquipment();
|
||||||
|
ce.setClassroomId(r.getId());
|
||||||
|
ce.setEquipmentId(eqId);
|
||||||
|
ce.setQuantity(qty);
|
||||||
|
ce.setSchId(building.getSchId());
|
||||||
|
classroomEquipmentRepository.save(ce);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (created == 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"未生成任何教室:编号均与楼栋内已有教室重复(前缀 " + prefix + ")");
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("created", created);
|
||||||
|
result.put("skipped", skipped);
|
||||||
|
result.put("total", created + skipped);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void edit(ClassroomEditRequest req) {
|
||||||
|
if (req == null || req.getSerID() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室ID不得为空");
|
||||||
|
}
|
||||||
|
String name = req.getRoomName() != null && !req.getRoomName().trim().isEmpty()
|
||||||
|
? req.getRoomName().trim() : null;
|
||||||
|
String code = req.getRoomCode() != null && !req.getRoomCode().trim().isEmpty()
|
||||||
|
? req.getRoomCode().trim() : null;
|
||||||
|
Classroom r = loadOwned(req.getSerID());
|
||||||
|
if (code != null && classroomRepository.existsBySchIdAndBuildingIdAndRoomCodeAndIdNot(
|
||||||
|
r.getSchId(), r.getBuildingId(), code, r.getId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该楼栋内教室编号已存在");
|
||||||
|
}
|
||||||
|
if (req.getStatus() != null && !STATUS_OPEN.equals(req.getStatus()) && !STATUS_CLOSED.equals(req.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室状态只支持 open/closed");
|
||||||
|
}
|
||||||
|
if (req.getTypeId() != null) {
|
||||||
|
ClassroomType type = typeRepository.findById(req.getTypeId()).orElse(null);
|
||||||
|
if (type == null || !r.getSchId().equals(type.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室类型不存在");
|
||||||
|
}
|
||||||
|
r.setTypeId(type.getId());
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
r.setRoomName(name);
|
||||||
|
}
|
||||||
|
if (code != null) {
|
||||||
|
r.setRoomCode(code);
|
||||||
|
}
|
||||||
|
if (req.getStatus() != null) {
|
||||||
|
r.setStatus(req.getStatus());
|
||||||
|
}
|
||||||
|
if (req.getCapacity() != null) {
|
||||||
|
if (req.getCapacity() < 0 || req.getCapacity() > 100000) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "容纳人数不正确");
|
||||||
|
}
|
||||||
|
r.setCapacity(req.getCapacity());
|
||||||
|
}
|
||||||
|
if (req.getSeatCount() != null) {
|
||||||
|
if (req.getSeatCount() < 0 || req.getSeatCount() > 100000) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "座位数不正确");
|
||||||
|
}
|
||||||
|
r.setSeatCount(req.getSeatCount());
|
||||||
|
}
|
||||||
|
classroomRepository.save(r);
|
||||||
|
// 设备关联:equipments/equipmentIds 均为 null=不改动;任一非 null(含空)=全量替换
|
||||||
|
if (req.getEquipments() != null || req.getEquipmentIds() != null) {
|
||||||
|
Map<Integer, Integer> equipmentMap =
|
||||||
|
sanitizeEquipmentMap(req.getEquipments(), req.getEquipmentIds(), r.getSchId());
|
||||||
|
classroomEquipmentRepository.deleteBySchIdAndClassroomId(r.getSchId(), r.getId());
|
||||||
|
equipmentMap.forEach((eqId, qty) -> {
|
||||||
|
com.yunhai.acaclassroom.entity.ClassroomEquipment ce =
|
||||||
|
new com.yunhai.acaclassroom.entity.ClassroomEquipment();
|
||||||
|
ce.setClassroomId(r.getId());
|
||||||
|
ce.setEquipmentId(eqId);
|
||||||
|
ce.setQuantity(qty);
|
||||||
|
ce.setSchId(r.getSchId());
|
||||||
|
classroomEquipmentRepository.save(ce);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析设备关联为 有序 map:设备 id → 每间数量。
|
||||||
|
* equipments 优先(数量缺省/非法按 1,上限 999;重复 id 取最大数量),否则回退 equipmentIds(数量按 1);
|
||||||
|
* 仅保留本校存在的设备。
|
||||||
|
*/
|
||||||
|
private Map<Integer, Integer> sanitizeEquipmentMap(
|
||||||
|
List<com.yunhai.acaclassroom.model.ClassroomEquipmentItem> equipments,
|
||||||
|
List<Integer> legacyEquipmentIds, Integer schId) {
|
||||||
|
Map<Integer, Integer> result = new LinkedHashMap<>();
|
||||||
|
if (equipments != null) {
|
||||||
|
for (com.yunhai.acaclassroom.model.ClassroomEquipmentItem item : equipments) {
|
||||||
|
if (item == null || item.getEquipmentId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int qty = item.getQuantity() == null || item.getQuantity() < 1
|
||||||
|
? 1 : Math.min(item.getQuantity(), 999);
|
||||||
|
result.merge(item.getEquipmentId(), qty, Math::max);
|
||||||
|
}
|
||||||
|
} else if (legacyEquipmentIds != null) {
|
||||||
|
legacyEquipmentIds.stream().filter(Objects::nonNull).forEach(id -> result.put(id, 1));
|
||||||
|
}
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Set<Integer> valid = equipmentRepository.findBySchId(schId).stream()
|
||||||
|
.map(com.yunhai.acaclassroom.entity.Equipment::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
result.keySet().retainAll(valid);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(List<Integer> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请选择要删除的教室");
|
||||||
|
}
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
// 设备关联随教室删除一并清理(设备台账本身保留)
|
||||||
|
classroomEquipmentRepository.deleteBySchIdAndClassroomIdIn(schId, ids);
|
||||||
|
classroomRepository.deleteBySchIdAndIdIn(schId, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> batchEdit(ClassroomBatchEditRequest req) {
|
||||||
|
if (req == null || req.getSerIDs() == null || req.getSerIDs().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请选择要修改的教室");
|
||||||
|
}
|
||||||
|
boolean changeStatus = req.getStatus() != null && !req.getStatus().trim().isEmpty();
|
||||||
|
boolean changeType = req.getTypeId() != null;
|
||||||
|
if (!changeStatus && !changeType) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请至少指定要修改的项(教室状态或教室类型)");
|
||||||
|
}
|
||||||
|
String status = changeStatus ? req.getStatus().trim() : null;
|
||||||
|
if (changeStatus && !STATUS_OPEN.equals(status) && !STATUS_CLOSED.equals(status)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室状态只支持 open/closed");
|
||||||
|
}
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
if (changeType) {
|
||||||
|
ClassroomType type = typeRepository.findById(req.getTypeId()).orElse(null);
|
||||||
|
if (type == null || !schId.equals(type.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室类型不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 去重后逐条处理:不存在 / 跨校的 id 计入 skipped,不整批失败(与批量添加的跳过语义一致)
|
||||||
|
Set<Integer> ids = new LinkedHashSet<>(req.getSerIDs());
|
||||||
|
int updated = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
for (Integer id : ids) {
|
||||||
|
if (id == null) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Classroom r = classroomRepository.findById(id).orElse(null);
|
||||||
|
if (r == null || !schId.equals(r.getSchId())) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (changeStatus) {
|
||||||
|
r.setStatus(status);
|
||||||
|
}
|
||||||
|
if (changeType) {
|
||||||
|
r.setTypeId(req.getTypeId());
|
||||||
|
}
|
||||||
|
classroomRepository.save(r);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("updated", updated);
|
||||||
|
result.put("skipped", skipped);
|
||||||
|
result.put("total", ids.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> bindClass(Integer serID, Integer classId) {
|
||||||
|
if (serID == null || classId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室ID与班级ID不得为空");
|
||||||
|
}
|
||||||
|
Classroom r = loadOwned(serID);
|
||||||
|
com.yunhai.acaclassroom.entity.SchoolClass c = classRepository.findById(classId).orElse(null);
|
||||||
|
if (c == null || !r.getSchId().equals(c.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "班级不存在或无权操作");
|
||||||
|
}
|
||||||
|
// 一个班级同时只能绑一间教室
|
||||||
|
classroomRepository.findBySchIdAndClassIdIn(r.getSchId(), Collections.singletonList(classId)).stream()
|
||||||
|
.filter(other -> !other.getId().equals(r.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(other -> {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该班级已绑定教室「" + other.getRoomName() + "」,请先解绑");
|
||||||
|
});
|
||||||
|
r.setClassId(classId);
|
||||||
|
classroomRepository.save(r);
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("classId", classId);
|
||||||
|
result.put("className", c.getClassName());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void unbindClass(Integer serID) {
|
||||||
|
if (serID == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室ID不得为空");
|
||||||
|
}
|
||||||
|
Classroom r = loadOwned(serID);
|
||||||
|
if (r.getClassId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.setClassId(null);
|
||||||
|
classroomRepository.save(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> batchUnbind(List<Integer> serIDs) {
|
||||||
|
if (serIDs == null || serIDs.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请选择要解绑的教室");
|
||||||
|
}
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
int updated = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
for (Integer id : new LinkedHashSet<>(serIDs)) {
|
||||||
|
if (id == null) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Classroom r = classroomRepository.findById(id).orElse(null);
|
||||||
|
if (r == null || !schId.equals(r.getSchId())) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r.getClassId() != null) {
|
||||||
|
r.setClassId(null);
|
||||||
|
classroomRepository.save(r);
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("updated", updated);
|
||||||
|
result.put("skipped", skipped);
|
||||||
|
result.put("total", serIDs.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室列表排序。
|
||||||
|
* sortField 为空或未知时**原样返回**(保持仓储层默认序:楼层升序 + 编号升序),
|
||||||
|
* 因此不传排序参数的既有调用方行为完全不变。
|
||||||
|
* 支持字段:floor / roomCode / roomName / status / capacity / seatCount;order:asc(默认)/ desc。
|
||||||
|
*/
|
||||||
|
private List<Classroom> applySort(List<Classroom> rooms, String sortField, String order) {
|
||||||
|
if (rooms == null || rooms.isEmpty() || sortField == null || sortField.trim().isEmpty()) {
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
Comparator<String> strCmp = Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
Comparator<Integer> intCmp = Comparator.nullsLast(Comparator.<Integer>naturalOrder());
|
||||||
|
Comparator<Classroom> cmp;
|
||||||
|
switch (sortField.trim()) {
|
||||||
|
case "roomCode":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getRoomCode(), strCmp);
|
||||||
|
break;
|
||||||
|
case "roomName":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getRoomName(), strCmp);
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getStatus(), strCmp);
|
||||||
|
break;
|
||||||
|
case "capacity":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getCapacity(), intCmp);
|
||||||
|
break;
|
||||||
|
case "seatCount":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getSeatCount(), intCmp);
|
||||||
|
break;
|
||||||
|
case "floor":
|
||||||
|
cmp = Comparator.comparing((Classroom r) -> r.getFloor(), intCmp)
|
||||||
|
.thenComparing((Classroom r) -> r.getRoomCode(), strCmp);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
List<Classroom> sorted = new ArrayList<>(rooms);
|
||||||
|
sorted.sort("desc".equalsIgnoreCase(order == null ? "" : order.trim()) ? cmp.reversed() : cmp);
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toVo(Classroom r, Map<Integer, String> typeNames) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", r.getId());
|
||||||
|
vo.put("buildingId", r.getBuildingId());
|
||||||
|
vo.put("campusId", r.getCampusId());
|
||||||
|
vo.put("floor", r.getFloor());
|
||||||
|
vo.put("roomName", r.getRoomName());
|
||||||
|
vo.put("roomCode", r.getRoomCode());
|
||||||
|
vo.put("typeId", r.getTypeId());
|
||||||
|
vo.put("typeName", r.getTypeId() != null ? typeNames.getOrDefault(r.getTypeId(), "") : "");
|
||||||
|
vo.put("status", r.getStatus());
|
||||||
|
vo.put("capacity", r.getCapacity());
|
||||||
|
vo.put("seatCount", r.getSeatCount());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Classroom loadOwned(Integer id) {
|
||||||
|
Classroom r = classroomRepository.findById(id).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (r == null || schId == null || !schId.equals(r.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "教室不存在或无权操作");
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Classroom;
|
||||||
|
import com.yunhai.acaclassroom.entity.ClassroomType;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomTypeRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.IClassroomTypeService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室类型管理服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClassroomTypeService implements IClassroomTypeService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomTypeRepository typeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomRepository classroomRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> list() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Map<Integer, Long> usedCount = classroomRepository.findBySchId(schId).stream()
|
||||||
|
.filter(r -> r.getTypeId() != null)
|
||||||
|
.collect(Collectors.groupingBy(Classroom::getTypeId, Collectors.counting()));
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (ClassroomType t : typeRepository.findBySchIdOrderBySortOrderAscIdAsc(schId)) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", t.getId());
|
||||||
|
vo.put("typeName", t.getTypeName());
|
||||||
|
vo.put("enableFlag", t.getEnableFlag());
|
||||||
|
vo.put("sortOrder", t.getSortOrder());
|
||||||
|
vo.put("classroomCount", usedCount.getOrDefault(t.getId(), 0L));
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Integer add(String typeName, Integer sortOrder) {
|
||||||
|
String name = requireText(typeName, "类型名称不得为空");
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
if (typeRepository.existsBySchIdAndTypeName(schId, name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "类型名称已存在");
|
||||||
|
}
|
||||||
|
ClassroomType t = new ClassroomType();
|
||||||
|
t.setTypeName(name);
|
||||||
|
t.setEnableFlag(1);
|
||||||
|
t.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
t.setSchId(schId);
|
||||||
|
return typeRepository.save(t).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void edit(Integer serID, String typeName, Integer enableFlag, Integer sortOrder) {
|
||||||
|
String name = requireText(typeName, "类型名称不得为空");
|
||||||
|
ClassroomType t = loadOwned(serID);
|
||||||
|
if (typeRepository.existsBySchIdAndTypeNameAndIdNot(t.getSchId(), name, serID)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "类型名称已存在");
|
||||||
|
}
|
||||||
|
t.setTypeName(name);
|
||||||
|
t.setEnableFlag(enableFlag != null && enableFlag == 0 ? 0 : 1);
|
||||||
|
t.setSortOrder(sortOrder != null ? sortOrder : 0);
|
||||||
|
typeRepository.save(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer serid) {
|
||||||
|
ClassroomType t = loadOwned(serid);
|
||||||
|
if (classroomRepository.countBySchIdAndTypeId(t.getSchId(), t.getId()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该类型已被教室使用,无法删除");
|
||||||
|
}
|
||||||
|
typeRepository.delete(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void sort(List<Integer> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ids.size(); i++) {
|
||||||
|
ClassroomType t = loadOwned(ids.get(i));
|
||||||
|
t.setSortOrder(i + 1);
|
||||||
|
typeRepository.save(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassroomType loadOwned(Integer id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "类型ID不得为空");
|
||||||
|
}
|
||||||
|
ClassroomType t = typeRepository.findById(id).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (t == null || schId == null || !schId.equals(t.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "类型不存在或无权操作");
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireSchId() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireText(String s, String message) {
|
||||||
|
if (s == null || s.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, message);
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.Equipment;
|
||||||
|
import com.yunhai.acaclassroom.repository.ClassroomEquipmentRepository;
|
||||||
|
import com.yunhai.acaclassroom.repository.EquipmentRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.IEquipmentService;
|
||||||
|
import com.yunhai.acaclassroom.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教室设备管理服务实现(独立设备台账)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class EquipmentService implements IEquipmentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EquipmentRepository equipmentRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClassroomEquipmentRepository classroomEquipmentRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> list(String keyword) {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
return emptyPage();
|
||||||
|
}
|
||||||
|
List<Equipment> rows = new ArrayList<>(equipmentRepository.findBySchIdOrderByIdDesc(schId));
|
||||||
|
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||||
|
String kw = keyword.trim().toLowerCase(Locale.ROOT);
|
||||||
|
rows = rows.stream().filter(e ->
|
||||||
|
e.getEquipmentName() != null && e.getEquipmentName().toLowerCase(Locale.ROOT).contains(kw))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用统计:教室数与使用数量(quantity 列为后加,存量 NULL 按 1)均按设备聚合
|
||||||
|
Map<Integer, Long> refCount = new HashMap<>();
|
||||||
|
Map<Integer, Integer> usedQty = new HashMap<>();
|
||||||
|
for (com.yunhai.acaclassroom.entity.ClassroomEquipment ce : classroomEquipmentRepository.findBySchId(schId)) {
|
||||||
|
refCount.merge(ce.getEquipmentId(), 1L, Long::sum);
|
||||||
|
usedQty.merge(ce.getEquipmentId(), ce.getQuantity() == null ? 1 : ce.getQuantity(), Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
for (Equipment e : rows) {
|
||||||
|
Map<String, Object> vo = toVo(e);
|
||||||
|
vo.put("quantity", usedQty.getOrDefault(e.getId(), 0));
|
||||||
|
vo.put("classroomCount", refCount.getOrDefault(e.getId(), 0L));
|
||||||
|
list.add(vo);
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", list);
|
||||||
|
result.put("total", list.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Integer add(String equipmentName, String remark) {
|
||||||
|
String name = requireText(equipmentName, "设备名称不得为空");
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
if (equipmentRepository.existsBySchIdAndEquipmentName(schId, name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "设备名称已存在");
|
||||||
|
}
|
||||||
|
Equipment e = new Equipment();
|
||||||
|
e.setEquipmentName(name);
|
||||||
|
e.setRemark(trimRemark(remark));
|
||||||
|
e.setSchId(schId);
|
||||||
|
return equipmentRepository.save(e).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void edit(Integer serID, String equipmentName, String remark) {
|
||||||
|
String name = requireText(equipmentName, "设备名称不得为空");
|
||||||
|
Equipment e = loadOwned(serID);
|
||||||
|
if (equipmentRepository.existsBySchIdAndEquipmentNameAndIdNot(e.getSchId(), name, e.getId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "设备名称已存在");
|
||||||
|
}
|
||||||
|
e.setEquipmentName(name);
|
||||||
|
e.setRemark(trimRemark(remark));
|
||||||
|
equipmentRepository.save(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer serid) {
|
||||||
|
Equipment e = loadOwned(serid);
|
||||||
|
if (classroomEquipmentRepository.existsBySchIdAndEquipmentId(e.getSchId(), e.getId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该设备已被教室使用,无法删除");
|
||||||
|
}
|
||||||
|
equipmentRepository.delete(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toVo(Equipment e) {
|
||||||
|
Map<String, Object> vo = new LinkedHashMap<>();
|
||||||
|
vo.put("serID", e.getId());
|
||||||
|
vo.put("equipmentName", e.getEquipmentName());
|
||||||
|
vo.put("remark", e.getRemark());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Equipment loadOwned(Integer id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "设备ID不得为空");
|
||||||
|
}
|
||||||
|
Equipment e = equipmentRepository.findById(id).orElse(null);
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (e == null || schId == null || !schId.equals(e.getSchId())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "设备不存在或无权操作");
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimRemark(String remark) {
|
||||||
|
if (remark == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String t = remark.trim();
|
||||||
|
return t.isEmpty() ? null : (t.length() > 200 ? t.substring(0, 200) : t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> emptyPage() {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", Collections.emptyList());
|
||||||
|
result.put("total", 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireSchId() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "未能获取当前学校信息,请重新登录");
|
||||||
|
}
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireText(String s, String message) {
|
||||||
|
if (s == null || s.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, message);
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.entity.SysOperationLog;
|
||||||
|
import com.yunhai.acaclassroom.repository.SysOperationLogRepository;
|
||||||
|
import com.yunhai.acaclassroom.service.ISysOperationLogService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志服务实现:异步落库(参照 UserclassService SysOperationLogServiceImpl.saveAsync)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class SysOperationLogService implements ISysOperationLogService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SysOperationLogService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysOperationLogRepository sysOperationLogRepository;
|
||||||
|
|
||||||
|
@Async("auditLogExecutor")
|
||||||
|
@Override
|
||||||
|
public void saveAsync(SysOperationLog entity) {
|
||||||
|
try {
|
||||||
|
sysOperationLogRepository.save(entity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[SysOperationLog] 异步保存失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package com.yunhai.acaclassroom.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.acaclassroom.service.IUserCenterService;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户中心门面服务实现:GetUserMod 模块树组装。
|
||||||
|
* <p>跨仓调用统一收敛到 {@link YschBaseOpenApi} 门面,Controller 不直接静态调用。</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UserCenterService implements IUserCenterService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(UserCenterService.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地联调开关(与 ControllerUserContextAspect 同键):免 SSO 联调时平台用户中心
|
||||||
|
* 里不存在开发身份(99999),getUserModule 必然返回空,前端会切成单应用模式
|
||||||
|
* 隐藏左侧菜单。开关开启且平台返回空时,回退本地默认模块树(modId 与前端
|
||||||
|
* routes.ts 的 meta.modId 契约一致)。生产严禁开启,不受影响。
|
||||||
|
*/
|
||||||
|
@Value("${acaclassroom.dev-context.enabled:false}")
|
||||||
|
private boolean devContextEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dev-context 联调账号(平台登录名,如 lijing):开关开启且无 CAS 会话时,
|
||||||
|
* 按此账号查平台用户中心取真实身份(姓名/头像/学校),顶栏与审计身份均为真实值。
|
||||||
|
* 环境变量:{@code ACACLASSROOM_DEVCONTEXT_LOGINNAME}。不配则 GetUserInfo 返回空(前端显示"管理员")。
|
||||||
|
*/
|
||||||
|
@Value("${acaclassroom.dev-context.login-name:}")
|
||||||
|
private String devLoginName;
|
||||||
|
|
||||||
|
/** dev 身份缓存(进程内):正面 5 分钟,查无此人/异常负缓存 60 秒,避免每请求打一次平台接口 */
|
||||||
|
private static final long DEV_USER_TTL_MS = 5 * 60 * 1000L;
|
||||||
|
private static final long DEV_USER_NEG_TTL_MS = 60 * 1000L;
|
||||||
|
private volatile BoaUser devUserCache;
|
||||||
|
/** 0 = 从未加载(不能用 Long.MIN_VALUE:now - MIN_VALUE 溢出为负会被误判成缓存命中) */
|
||||||
|
private volatile long devUserCacheAt = 0L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getUserModuleTree(Integer userId, Integer schId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "用户ID不得为空");
|
||||||
|
}
|
||||||
|
List<BoaModule> modules = YschBaseOpenApi.getUserModule(userId, schId);
|
||||||
|
if (modules == null || modules.isEmpty()) {
|
||||||
|
if (devContextEnabled) {
|
||||||
|
log.info("[GetUserMod] 平台未返回模块(本地联调身份 userId={}),回退本地默认模块树", userId);
|
||||||
|
return buildModuleTree(defaultModules());
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 扁平 BoaModule → 树结构(modId/page/children),对齐前端 layout 期望
|
||||||
|
return buildModuleTree(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BoaUser getCurrentUser(BoaUser casUser) {
|
||||||
|
if (casUser != null) {
|
||||||
|
return casUser;
|
||||||
|
}
|
||||||
|
if (!devContextEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolveDevUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动即打印 dev 联调身份解析入参,便于排查环境变量是否绑定成功(仅 dev-context 开启时输出)。 */
|
||||||
|
@javax.annotation.PostConstruct
|
||||||
|
void logDevContext() {
|
||||||
|
if (devContextEnabled) {
|
||||||
|
log.info("[DevContext] 免SSO联调开关已开启,联调账号 acaclassroom.dev-context.login-name='{}'",
|
||||||
|
devLoginName == null ? "" : devLoginName.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 {@code dev-context.login-name} 查平台用户中心取真实 dev 身份(带缓存)。
|
||||||
|
* 未配置/查无此人/平台异常均返回 null(负缓存避免联调期反复打失败请求)。
|
||||||
|
*/
|
||||||
|
private BoaUser resolveDevUser() {
|
||||||
|
String account = devLoginName == null ? "" : devLoginName.trim();
|
||||||
|
if (account.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
BoaUser cached = devUserCache;
|
||||||
|
long loadedAt = devUserCacheAt;
|
||||||
|
long ttl = cached != null ? DEV_USER_TTL_MS : DEV_USER_NEG_TTL_MS;
|
||||||
|
if (loadedAt != 0L && now - loadedAt < ttl) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (devUserCacheAt != 0L
|
||||||
|
&& System.currentTimeMillis() - devUserCacheAt < (devUserCache != null ? DEV_USER_TTL_MS : DEV_USER_NEG_TTL_MS)) {
|
||||||
|
return devUserCache;
|
||||||
|
}
|
||||||
|
BoaUser user = null;
|
||||||
|
try {
|
||||||
|
user = YschBaseOpenApi.getUserByLoginName(account);
|
||||||
|
if (user == null) {
|
||||||
|
log.warn("[DevContext] 平台用户中心未查到账号 {},GetUserInfo 返回空(60s 内不再重试)", account);
|
||||||
|
} else {
|
||||||
|
log.info("[DevContext] dev 身份解析为平台账号 {}:id={} name={}", account, user.getId(), user.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[DevContext] 平台用户中心查询账号 {} 失败:{}(60s 内不再重试)", account, e.getMessage());
|
||||||
|
}
|
||||||
|
devUserCache = user;
|
||||||
|
devUserCacheAt = System.currentTimeMillis();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本地联调默认模块树:教室管理 → 绑定班级 → 基础设置(顺序即左侧菜单顺序)。 */
|
||||||
|
private List<BoaModule> defaultModules() {
|
||||||
|
BoaModule classroom = new BoaModule();
|
||||||
|
classroom.setId(41);
|
||||||
|
classroom.setParentId(0);
|
||||||
|
classroom.setName("教室管理");
|
||||||
|
classroom.setPath("/classroom");
|
||||||
|
classroom.setLevel(1);
|
||||||
|
classroom.setSortNo(1);
|
||||||
|
classroom.setIcon("");
|
||||||
|
|
||||||
|
BoaModule classBind = new BoaModule();
|
||||||
|
classBind.setId(43);
|
||||||
|
classBind.setParentId(0);
|
||||||
|
classBind.setName("绑定班级");
|
||||||
|
classBind.setPath("/classbind");
|
||||||
|
classBind.setLevel(1);
|
||||||
|
classBind.setSortNo(2);
|
||||||
|
classBind.setIcon("");
|
||||||
|
|
||||||
|
BoaModule setting = new BoaModule();
|
||||||
|
setting.setId(42);
|
||||||
|
setting.setParentId(0);
|
||||||
|
setting.setName("基础设置");
|
||||||
|
setting.setPath("/setting");
|
||||||
|
setting.setLevel(1);
|
||||||
|
setting.setSortNo(3);
|
||||||
|
setting.setIcon("");
|
||||||
|
|
||||||
|
List<BoaModule> list = new ArrayList<>();
|
||||||
|
list.add(classroom);
|
||||||
|
list.add(classBind);
|
||||||
|
list.add(setting);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将扁平模块列表组装为树。
|
||||||
|
* 顶层取 parentId=0 或父节点不在列表中的节点,保持 SDK(用户中心)返回顺序。
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
|
||||||
|
Set<Integer> moduleIds = new HashSet<>();
|
||||||
|
for (BoaModule m : modules) {
|
||||||
|
moduleIds.add(m.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每个模块节点(含 children 占位)
|
||||||
|
Map<Integer, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||||
|
for (BoaModule m : modules) {
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", m.getId());
|
||||||
|
node.put("modId", m.getId());
|
||||||
|
node.put("name", m.getName());
|
||||||
|
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
|
||||||
|
node.put("page", m.getPath() != null ? m.getPath() : "");
|
||||||
|
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
|
||||||
|
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
|
||||||
|
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
|
||||||
|
node.put("usable", true);
|
||||||
|
node.put("children", new ArrayList<>());
|
||||||
|
nodeMap.put(m.getId(), node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂父子关系
|
||||||
|
List<Map<String, Object>> roots = new ArrayList<>();
|
||||||
|
for (BoaModule m : modules) {
|
||||||
|
Map<String, Object> node = nodeMap.get(m.getId());
|
||||||
|
int parentId = m.getParentId() != null ? m.getParentId() : 0;
|
||||||
|
if (parentId == 0 || !moduleIds.contains(parentId)) {
|
||||||
|
roots.add(node);
|
||||||
|
} else {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> children = (List<Map<String, Object>>) nodeMap.get(parentId).get("children");
|
||||||
|
children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roots.isEmpty()) {
|
||||||
|
log.warn("[GetUserMod] 模块列表无根节点, count={}", modules.size());
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.yunhai.acaclassroom.utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GZIP 压缩工具(参照 UserclassService GzipUtil),用于操作日志响应体压缩存储。
|
||||||
|
*/
|
||||||
|
public final class GzipUtil {
|
||||||
|
|
||||||
|
private GzipUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字符串 → GZIP 字节。失败返回 null(调用方按可空处理,不影响主业务)。 */
|
||||||
|
public static byte[] compress(String data) {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length() / 2)) {
|
||||||
|
try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
|
||||||
|
gzip.write(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
return bos.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GZIP 字节 → 字符串。失败返回 null。 */
|
||||||
|
public static String decompress(byte[] data) {
|
||||||
|
if (data == null || data.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
|
||||||
|
GZIPInputStream gzip = new GZIPInputStream(bis);
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int n;
|
||||||
|
while ((n = gzip.read(buf)) != -1) {
|
||||||
|
bos.write(buf, 0, n);
|
||||||
|
}
|
||||||
|
return new String(bos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.yunhai.acaclassroom.utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求的用户上下文(ThreadLocal),参照 UserclassService 的自有 UserContext。
|
||||||
|
* <p>由 {@link com.yunhai.acaclassroom.aspect.ControllerUserContextAspect} 在控制器执行前,
|
||||||
|
* 从基础平台 {@code BoaUserContext.get()}(或控制器 {@code BoaUser} 入参)复制填充,请求结束 {@link #clear()}。</p>
|
||||||
|
*
|
||||||
|
* <p>业务约定:实体只存数字 id,不存登录账号。
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #getUserId()} —— 平台用户数字 id(教师用户),业务外键/审计列存此值;</li>
|
||||||
|
* <li>{@link #getSchId()} —— 当前身份所属学校,多租户隔离。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class UserContext {
|
||||||
|
|
||||||
|
private static final ThreadLocal<UserInfo> HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private UserContext() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set(UserInfo info) {
|
||||||
|
HOLDER.set(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserInfo get() {
|
||||||
|
return HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
HOLDER.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 便捷读取(无上下文时返回 null,调用方按需判空/兜底) ============
|
||||||
|
|
||||||
|
/** 平台用户数字 id(教师用户统一用它做业务外键,不存账号) */
|
||||||
|
public static Integer getUserId() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示姓名(非登录账号) */
|
||||||
|
public static String getUserName() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Integer getSchId() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.schId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Integer getRoleId() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.roleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Integer getType() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getIp() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null ? null : i.ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 不可变快照。 */
|
||||||
|
public static final class UserInfo {
|
||||||
|
public Integer userId;
|
||||||
|
public String userName;
|
||||||
|
public Integer schId;
|
||||||
|
public Integer roleId;
|
||||||
|
public Integer type;
|
||||||
|
public String ip;
|
||||||
|
|
||||||
|
public UserInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserInfo(Integer userId, String userName, Integer schId, Integer roleId, Integer type, String ip) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.userName = userName;
|
||||||
|
this.schId = schId;
|
||||||
|
this.roleId = roleId;
|
||||||
|
this.type = type;
|
||||||
|
this.ip = ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
server:
|
||||||
|
port: 8106
|
||||||
|
|
||||||
|
spring:
|
||||||
|
mvc:
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
jackson:
|
||||||
|
time-zone: GMT+8
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/aca_classroom?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci&serverTimezone=Asia/Shanghai
|
||||||
|
username: root
|
||||||
|
password: 123123
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
min-idle: 10
|
||||||
|
max-active: 30
|
||||||
|
#jpa
|
||||||
|
jpa:
|
||||||
|
database: MySQL
|
||||||
|
show-sql: true
|
||||||
|
hibernate:
|
||||||
|
# 开发期使用 update 自动建表;生产环境设置成 none
|
||||||
|
ddl-auto: update
|
||||||
|
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||||
|
properties:
|
||||||
|
hibernate.order_by.default_null_ordering: last
|
||||||
|
open-in-view: true
|
||||||
|
|
||||||
|
#文件上传
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-request-size: 100MB
|
||||||
|
|
||||||
|
#本地关闭 rabbit
|
||||||
|
rabbitmq:
|
||||||
|
listener:
|
||||||
|
direct:
|
||||||
|
auto-startup: false
|
||||||
|
simple:
|
||||||
|
auto-startup: false
|
||||||
|
|
||||||
|
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
|
||||||
|
yh:
|
||||||
|
sso:
|
||||||
|
server-url-prefix: http://sso.yunhaixingtong.com
|
||||||
|
client-host-url: http://localhost:8106
|
||||||
|
allow-headers: all
|
||||||
|
allow-origins: all
|
||||||
|
url-pattern: /api/*
|
||||||
|
mobi-api-pattern: /mobiapi/*
|
||||||
|
app-auth:
|
||||||
|
enabled: true
|
||||||
|
# 本应用自身身份(客户端签名用)
|
||||||
|
app-key: 100024
|
||||||
|
app-secret: 4492df661f3740d38e68ee954cebdfba
|
||||||
|
open:
|
||||||
|
enabled: true
|
||||||
|
base-host: http://base.yunhaixingtong.com
|
||||||
|
user-center-url: http://base.yunhaixingtong.com/userbase
|
||||||
|
# 班级管理(bschclass)服务基址
|
||||||
|
bschclass: http://base.yunhaixingtong.com/userclass
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: acaclassroom
|
||||||
|
profiles:
|
||||||
|
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||||
|
cloud:
|
||||||
|
nacos:
|
||||||
|
config:
|
||||||
|
enabled: ${NACOS_CONFIG_ENABLED:false}
|
||||||
|
server-addr: ${NACOS_ADDR:nacos.yunhaixingtong.com:8848}
|
||||||
|
namespace: ${SPRING_CLOUD_NACOS_CONFIG_NAMESPACE:6196f0de-a28d-4894-8f10-e2982b57c6bc}
|
||||||
|
# 应用自身配置所在分组
|
||||||
|
group: OA_APP
|
||||||
|
# 配置文件格式;最终主配置 dataId = ${spring.application.name}.${file-extension},即 notice.yaml
|
||||||
|
file-extension: yaml
|
||||||
|
encode: UTF-8
|
||||||
|
# nacos 不可达时不强制退出,回退到本地 application.yml 兜底
|
||||||
|
fail-fast: false
|
||||||
|
# 只读账号
|
||||||
|
username: ${SPRING_CLOUD_NACOS_USERNAME:yh-reader}
|
||||||
|
password: ${SPRING_CLOUD_NACOS_PASSWORD:123456}
|
||||||
|
# 共享公共配置:public 空间下的 common.properties
|
||||||
|
shared-configs:
|
||||||
|
# ① 1 号(最低优先级):public 命名空间(namespace="" 固定写空字符串)的 application-default.yaml
|
||||||
|
- data-id: application-default.yaml
|
||||||
|
group: DEFAULT_GROUP
|
||||||
|
namespace: "" # ⭐ 固定 = "",强制指向 Nacos "public" 命名空间,不读上面的全局 prod namespace
|
||||||
|
refresh: false
|
||||||
|
|
||||||
|
# ② 2 号(中等优先级):prod 命名空间(不写 namespace = 继承顶部全局 prod namespace id)的 application-infra.yaml
|
||||||
|
- data-id: application-infra.yaml
|
||||||
|
group: DEFAULT_GROUP
|
||||||
|
refresh: false
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<!-- 引入 Spring Boot 默认彩色日志变量 -->
|
||||||
|
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||||
|
|
||||||
|
<!-- 控制台:所有环境都有,且带颜色2 -->
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 文件日志:只有非 local 环境才启用 -->
|
||||||
|
<springProfile name="!local">
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>/app/logs/app.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
<totalSizeCap>5GB</totalSizeCap>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE" />
|
||||||
|
<springProfile name="!local">
|
||||||
|
<appender-ref ref="FILE" />
|
||||||
|
</springProfile>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- 教室管理(acaclassroom)初始化脚本:仅建库;表结构由 JPA ddl-auto=update 自动创建。
|
||||||
|
-- 本地:mysql -uroot -p123123 < acaclassroom_init.sql
|
||||||
|
CREATE DATABASE IF NOT EXISTS aca_classroom DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||||
|
USE aca_classroom;
|
||||||
|
|
||||||
|
-- 种子数据(可选):ddl-auto 建表后执行,或由页面手工维护。
|
||||||
|
-- INSERT INTO aca_classroom_type (type_name, enable_flag, sort_order, sch_id, create_time, update_time) VALUES
|
||||||
|
-- ('普通教室', 1, 1, <schId>, NOW(), NOW()),
|
||||||
|
-- ('多媒体教室', 1, 2, <schId>, NOW(), NOW()),
|
||||||
|
-- ('实验室', 1, 3, <schId>, NOW(), NOW());
|
||||||
Reference in New Issue
Block a user