commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
# 本地纯前端调试模式:仅 vite dev --mode mock 加载(build 产物不读任何 .env.*,
|
||||||
|
# 线上由 /api/env-config?target=front 运行时下发)。后端 SSO/base 不可达时启用内置 mock
|
||||||
|
# 启动:npm run dev:mock
|
||||||
|
VITE_BASE_HOST=
|
||||||
|
VITE_SSO_HOST=
|
||||||
|
VITE_SERVICE_HOST=http://localhost:8102
|
||||||
|
VITE_ZIYUANKU_HOST=
|
||||||
|
VITE_ZHUANXIANG_HOST=
|
||||||
|
VITE_IS_MOCK=true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
|
||||||
|
import DormMock from "./dormapi";
|
||||||
|
|
||||||
|
export function setupProdMockServer() {
|
||||||
|
createProdMockServer([...DormMock]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { IModule } from "@/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 家校宿舍通知 左侧菜单数据(对照后端 GetUserMod 现行模块)。
|
||||||
|
* 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。
|
||||||
|
*/
|
||||||
|
export function getMenuList(): Promise<IModule[]> {
|
||||||
|
const menu: IModule[] = [
|
||||||
|
{ id: 2, modId: 2, name: "我发布的", page: "/list" },
|
||||||
|
{ id: 3, modId: 3, name: "全部通知", page: "/noticelist" },
|
||||||
|
{ id: 25, modId: 25, name: "回执情况", page: "/receipts" },
|
||||||
|
{ id: 4, modId: 4, name: "通知统计", page: "/stats" },
|
||||||
|
{ id: 21, modId: 21, name: "宿舍设置", page: "/setting" },
|
||||||
|
{ id: 31, modId: 31, name: "宿舍通知箱(家长端预览)", page: "/par/notices" },
|
||||||
|
];
|
||||||
|
return Promise.resolve(menu);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 由 projman 注入的 Spring Boot 运行镜像(单阶段,jar 预构建)
|
||||||
|
# CI 已先 mvn package,并在容器内把产物 cp 成固定名 target/app.jar
|
||||||
|
# 构建上下文 = 仓库根:docker build -f docker/Dockerfile .
|
||||||
|
# 健康检查依赖 curl(projman 部署用 curl 探 /actuator/health)——应用需含 spring-boot-starter-actuator
|
||||||
|
# 时区 Asia/Shanghai;JAVA_OPTS 由 projman 部署 env 注入(默认 -Xms512m -Xmx1024m)
|
||||||
|
FROM eclipse-temurin:8-jre
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl tzdata \
|
||||||
|
&& ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY target/app.jar /app/app.jar
|
||||||
|
|
||||||
|
EXPOSE 10002
|
||||||
|
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar"]
|
||||||
@@ -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/notice
|
||||||
|
context-path: /notice
|
||||||
|
|
||||||
|
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;库名为 dorm_notice(需先在 mysql8 上建库)
|
||||||
|
url: jdbc:mysql://mysql8:3306/dorm_notice?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/notice
|
||||||
|
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/notice.jar \
|
||||||
|
--spring.profiles.active=prod
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<?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>dormnotice</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<name>DormNotice</name>
|
||||||
|
<description>宿舍通知子应用</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>1.8</java.version>
|
||||||
|
</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>dormnotice</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.dormnotice.DormNoticeApplication</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:/notice-build/notice</directory>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 宿舍通知 家长端闭环自清理测试(P8 深测)。
|
||||||
|
* 教师建临时楼/房 → 把登录学生分配到该房 → 发布 confirm + fill 两条通知到该房 →
|
||||||
|
* 学生 MyNotice(自动确认)/SubmitReceipt(填报) → 教师 Receipts/Statis 校验 →
|
||||||
|
* finally 逆序彻底清理:删通知(级联删回执) → 学生退宿 → 删房间 → 删楼栋。
|
||||||
|
*
|
||||||
|
* 用法:TEACHER_COOKIE='…' STUDENT_COOKIE='…' [STUDENT_ID=10069] node smoke/closed-loop.mjs
|
||||||
|
*/
|
||||||
|
const BASE = (process.env.BASE || "http://localhost:8105").replace(/\/+$/, "");
|
||||||
|
const T = process.env.TEACHER_COOKIE || "";
|
||||||
|
const S = process.env.STUDENT_COOKIE || "";
|
||||||
|
const SID = Number(process.env.STUDENT_ID || 10069);
|
||||||
|
if (!T || !S) { console.error("需 TEACHER_COOKIE 与 STUDENT_COOKIE"); process.exit(2); }
|
||||||
|
|
||||||
|
const stamp = Date.now();
|
||||||
|
const BLD = "闭环测试楼" + stamp;
|
||||||
|
let buildingId = null, roomId = null, noticeConfirm = null, noticeFill = null;
|
||||||
|
let assigned = false;
|
||||||
|
const log = (ok, name, detail = "") => console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
|
||||||
|
|
||||||
|
async function call(cookie, method, path, { body, form } = {}) {
|
||||||
|
const headers = { Accept: "application/json", Cookie: cookie };
|
||||||
|
let payload;
|
||||||
|
if (body !== undefined) { headers["Content-Type"] = "application/json;charset=UTF-8"; payload = JSON.stringify(body); }
|
||||||
|
else if (form !== undefined) { headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"; payload = new URLSearchParams(form).toString(); }
|
||||||
|
const r = await fetch(BASE + path, { method, headers, body: payload, redirect: "manual" });
|
||||||
|
const txt = await r.text();
|
||||||
|
let j = null; try { j = JSON.parse(txt); } catch { /* non-json */ }
|
||||||
|
if (r.status !== 200 || !j || j.code !== 0) throw new Error(`${method} ${path} → HTTP ${r.status} ${txt.slice(0, 160)}`);
|
||||||
|
return j.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
// 楼栋
|
||||||
|
await call(T, "POST", "/api/dorm/AddBuilding", { form: { buildingName: BLD, gender: "混合", floorCount: 1, roomsPerFloor: 1, bedsPerRoom: 2 } });
|
||||||
|
const bl = await call(T, "GET", "/api/dorm/BuildingList");
|
||||||
|
buildingId = (bl.find(x => x.buildingName === BLD) || {}).buildingId;
|
||||||
|
if (!buildingId) throw new Error("楼栋未创建成功");
|
||||||
|
log(true, "dorm/AddBuilding", "buildingId=" + buildingId);
|
||||||
|
// 房间
|
||||||
|
await call(T, "POST", "/api/dorm/AddRoom", { form: { buildingId, floor: 1, roomNo: "998", bedCount: 2 } });
|
||||||
|
const rl = await call(T, "GET", "/api/dorm/RoomList?buildingId=" + buildingId);
|
||||||
|
roomId = (rl.find(x => x.roomNo === "998") || {}).roomId;
|
||||||
|
if (!roomId) throw new Error("房间未创建成功");
|
||||||
|
log(true, "dorm/AddRoom", "roomId=" + roomId);
|
||||||
|
// 分配学生(需在 dorm_student 池内)
|
||||||
|
await call(T, "POST", "/api/dorm/AssignStudent", { form: { roomId, studentIds: String(SID) } });
|
||||||
|
assigned = true;
|
||||||
|
const rs = await call(T, "GET", "/api/dorm/RoomStudents?roomId=" + roomId);
|
||||||
|
log(rs.some(s => s.studentId === SID), "dorm/AssignStudent", "房内=" + JSON.stringify(rs.map(s => s.studentId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// confirm 通知
|
||||||
|
noticeConfirm = await call(T, "POST", "/api/notice/Create", {
|
||||||
|
body: { title: "【闭环】需确认通知" + stamp, content: "<p>confirm</p>", noticeType: "安全排查", level: "important",
|
||||||
|
receiptMode: "confirm", deptId: "D001", receiveType: "room", receiveList: String(roomId), needSupervisor: 1, draft: false } });
|
||||||
|
log(!!noticeConfirm, "notice/Create(confirm)", "id=" + noticeConfirm);
|
||||||
|
// fill 通知
|
||||||
|
noticeFill = await call(T, "POST", "/api/notice/Create", {
|
||||||
|
body: { title: "【闭环】需填报通知" + stamp, content: "<p>fill</p>", noticeType: "留宿申请", level: "normal",
|
||||||
|
receiptMode: "fill", formType: "stay", deptId: "D001", receiveType: "room", receiveList: String(roomId), needSupervisor: 0, draft: false } });
|
||||||
|
log(!!noticeFill, "notice/Create(fill/stay)", "id=" + noticeFill);
|
||||||
|
|
||||||
|
// 学生收件箱应见两条
|
||||||
|
const mn = await call(S, "GET", "/api/notice/MyNotices");
|
||||||
|
const ids = (mn.list || []).map(x => x.id);
|
||||||
|
log(ids.includes(noticeConfirm) && ids.includes(noticeFill), "notice/MyNotices(学生可见两条)", "buckets=" + JSON.stringify(mn.buckets));
|
||||||
|
|
||||||
|
// 打开 confirm 通知 → 自动确认
|
||||||
|
const mc = await call(S, "GET", "/api/notice/MyNotice?serid=" + noticeConfirm);
|
||||||
|
log(mc.my && mc.my.status === "confirmed", "notice/MyNotice(confirm 打开即自动确认)", "status=" + (mc.my && mc.my.status));
|
||||||
|
|
||||||
|
// 填报 fill 通知
|
||||||
|
const sr = await call(S, "POST", "/api/notice/SubmitReceipt", {
|
||||||
|
body: { serid: noticeFill, guardianName: "测试家长", guardianPhone: "13900000000", relation: "父亲",
|
||||||
|
fillData: { conclusion: "同意", stayStart: "2026-10-01", stayEnd: "2026-10-03", stayCount: "1", stayReason: "家远不便往返" },
|
||||||
|
remark: "闭环自测" } });
|
||||||
|
log(sr && sr.status === "filled", "notice/SubmitReceipt(fill)", "status=" + (sr && sr.status));
|
||||||
|
|
||||||
|
// 教师看回执名单
|
||||||
|
const rc = await call(T, "GET", "/api/notice/Receipts?serid=" + noticeFill + "&status=all");
|
||||||
|
log(rc.summary.should >= 1 && rc.summary.filled >= 1, "notice/Receipts(fill 汇总)", "should=" + rc.summary.should + " filled=" + rc.summary.filled + " rate=" + rc.summary.receiptRate + "%");
|
||||||
|
const rc2 = await call(T, "GET", "/api/notice/Receipts?serid=" + noticeConfirm + "&status=all");
|
||||||
|
log(rc2.summary.confirmed >= 1, "notice/Receipts(confirm 汇总)", "confirmed=" + rc2.summary.confirmed);
|
||||||
|
|
||||||
|
// 阅读情况(房间级)
|
||||||
|
const rd = await call(T, "GET", "/api/notice/ReadDetail?serid=" + noticeConfirm);
|
||||||
|
log(Array.isArray(rd) && rd.length >= 1, "notice/ReadDetail", JSON.stringify(rd).slice(0, 120));
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const st = await call(T, "GET", "/api/notice/Statis");
|
||||||
|
log(st.publishedCount >= 2, "notice/Statis", "publishedCount=" + st.publishedCount + " totalReceipts=" + st.totalReceipts);
|
||||||
|
|
||||||
|
// 导出回执 CSV
|
||||||
|
const csv = await (await fetch(BASE + "/api/notice/ExportReceipts?serid=" + noticeFill, { headers: { Cookie: T } })).text();
|
||||||
|
log(/回执状态|留宿/.test(csv), "notice/ExportReceipts", "len=" + csv.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
const errs = [];
|
||||||
|
try { if (noticeConfirm || noticeFill) await call(T, "POST", "/api/notice/Delete", { body: [noticeConfirm, noticeFill].filter(Boolean) }); } catch (e) { errs.push("delNotice:" + e.message); }
|
||||||
|
try { if (assigned && roomId) await call(T, "POST", "/api/dorm/RemoveStudent", { form: { roomId, studentId: SID } }); } catch (e) { errs.push("removeStudent:" + e.message); }
|
||||||
|
try { if (roomId) await call(T, "POST", "/api/dorm/DelRoom", { form: { roomId } }); } catch (e) { errs.push("delRoom:" + e.message); }
|
||||||
|
try { if (buildingId) await call(T, "POST", "/api/dorm/DelBuilding", { form: { buildingId } }); } catch (e) { errs.push("delBuilding:" + e.message); }
|
||||||
|
// 校验清理干净
|
||||||
|
try {
|
||||||
|
const bl = await call(T, "GET", "/api/dorm/BuildingList");
|
||||||
|
const gone = !bl.some(x => x.buildingId === buildingId);
|
||||||
|
log(gone, "cleanup 楼栋已删", gone ? "" : "残留 buildingId=" + buildingId);
|
||||||
|
} catch (e) { errs.push("verify:" + e.message); }
|
||||||
|
if (errs.length) console.log("清理告警:", errs.join(" | "));
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
console.log(`\n=== 闭环自清理测试 @ ${BASE} · 学生 studentId=${SID} ===`);
|
||||||
|
let failed = 0;
|
||||||
|
try { await setup(); await run(); }
|
||||||
|
catch (e) { failed++; log(false, "流程异常", e.message); }
|
||||||
|
finally { console.log("\n--- 清理 ---"); await cleanup(); }
|
||||||
|
console.log(failed ? "\n结果:有失败" : "\n结果:闭环全部通过(数据已清理)");
|
||||||
|
process.exit(failed ? 1 : 0);
|
||||||
|
})();
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 宿舍通知(DormitoryNotice)后端 P8 端到端冒烟脚本。
|
||||||
|
*
|
||||||
|
* 背景:/api/* 受基础平台 CAS/SSO 过滤器保护,无登录态会返回 302→SSO,脚本无法自证身份。
|
||||||
|
* 因此用「浏览器登录后复制 Cookie」的方式携带会话:
|
||||||
|
* 1) 起后端:先跑 sql/dormnotice_init.sql 建库+字典,再 `java -jar target/dormnotice.jar`
|
||||||
|
* (或 IDEA 运行 DormNoticeApplication,端口 8105)。
|
||||||
|
* 2) 浏览器用教师账号 lijing 走 SSO 登录前端(http://localhost:8078 或后端直连),
|
||||||
|
* 打开 DevTools→Network→任一 /api 请求,复制其「Cookie」请求头整串值。
|
||||||
|
* 3) 同样用学生账号(kegemo / csxs1)登录,复制一份学生 Cookie。
|
||||||
|
* 4) 运行:
|
||||||
|
* TEACHER_COOKIE='<粘贴教师Cookie>' STUDENT_COOKIE='<粘贴学生Cookie>' \
|
||||||
|
* node smoke/smoke.mjs
|
||||||
|
* 可选:BASE(默认 http://localhost:8105)。
|
||||||
|
*
|
||||||
|
* 只读为主;Create 冒烟会创建一条草稿再删除,不留脏数据。退出码非 0 表示有必过项失败。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE = (process.env.BASE || "http://localhost:8105").replace(/\/+$/, "");
|
||||||
|
const TEACHER = process.env.TEACHER_COOKIE || "";
|
||||||
|
const STU = process.env.STUDENT_COOKIE || "";
|
||||||
|
// 写链路默认执行(用一次性数据并自清理);设 WRITE=0 跳过所有增删改,仅跑只读冒烟。
|
||||||
|
const WRITE = (process.env.WRITE || "1") !== "0";
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function hdr(cookie, extra = {}) {
|
||||||
|
const h = { Accept: "application/json", ...extra };
|
||||||
|
if (cookie) h.Cookie = cookie;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function req(label, method, path, { cookie = TEACHER, body, form, json = true, must = true } = {}) {
|
||||||
|
const url = BASE + path;
|
||||||
|
const headers = hdr(cookie);
|
||||||
|
let payload;
|
||||||
|
if (body !== undefined) { headers["Content-Type"] = "application/json;charset=UTF-8"; payload = JSON.stringify(body); }
|
||||||
|
else if (form !== undefined) { headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"; payload = new URLSearchParams(form).toString(); }
|
||||||
|
let status, data, raw;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method, headers, body: payload, redirect: "manual" });
|
||||||
|
status = resp.status;
|
||||||
|
raw = await resp.text();
|
||||||
|
if (json) { try { data = JSON.parse(raw); } catch { data = null; } }
|
||||||
|
} catch (e) {
|
||||||
|
push(label, false, `网络错误 ${e.message}`, must);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (status === 302 || status === 301) {
|
||||||
|
push(label, false, `${status}→SSO 重定向:Cookie 无效/未登录`, must);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
push(label, status === 200 && (!json || (data && data.code === 0)), `HTTP ${status} code=${data && data.code} msg=${data && data.msg}`, must);
|
||||||
|
return data ? data.data : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function push(name, ok, detail, must) {
|
||||||
|
results.push({ name, ok, detail, must });
|
||||||
|
if (!ok && must) failed++;
|
||||||
|
console.log(`${ok ? "PASS" : "FAIL"} ${name} — ${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`\n=== DormNotice 冒烟 @ ${BASE} ===`);
|
||||||
|
if (!TEACHER) { console.log("⚠ 未提供 TEACHER_COOKIE,鉴权类接口会 302。请先按脚本头注释获取。"); }
|
||||||
|
|
||||||
|
// 0. 免鉴权健康(actuator 返回 {status:UP},非 {code} 信封,只验 HTTP 200)
|
||||||
|
await req("actuator/health", "GET", "/actuator/health", { must: false, json: false });
|
||||||
|
|
||||||
|
// 1. 平台代理
|
||||||
|
await req("base/GetUserInfo", "GET", "/api/base/GetUserInfo");
|
||||||
|
await req("base/GetUserMod", "GET", "/api/base/GetUserMod");
|
||||||
|
await req("base/GetDormTree", "GET", "/api/base/GetDormTree", { must: false });
|
||||||
|
await req("base/SetRole(parent)", "POST", "/api/base/SetRole", { cookie: STU || TEACHER, body: { as: "parent" }, must: false });
|
||||||
|
|
||||||
|
// 2. 字典(B2:type 参数化 + 无“文字重复”)
|
||||||
|
const types = await req("dict/List?noticeType", "GET", "/api/dict/List?type=noticeType");
|
||||||
|
const depts = await req("dict/ManageList?dept", "GET", "/api/dict/ManageList?type=dept");
|
||||||
|
await req("dict/Sync", "GET", "/api/dict/Sync", { must: false });
|
||||||
|
// 断言无逗号重复名
|
||||||
|
if (Array.isArray(types)) {
|
||||||
|
const dup = types.some(t => String(t.v || t.k || "").includes(","));
|
||||||
|
push("字典名无“X,X”重复", !dup, dup ? "存在含逗号字典名,检查 B1 是否真后端未装" : "OK", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 宿舍域
|
||||||
|
const buildings = await req("dorm/BuildingList", "GET", "/api/dorm/BuildingList", { must: false });
|
||||||
|
await req("dorm/RoomList", "GET", "/api/dorm/RoomList", { must: false });
|
||||||
|
await req("dorm/PendingStudents", "GET", "/api/dorm/PendingStudents", { must: false });
|
||||||
|
|
||||||
|
// 4. 通知:FormTemplates / SendList / Create(草稿)→Detail→Delete
|
||||||
|
await req("notice/FormTemplates", "GET", "/api/notice/FormTemplates");
|
||||||
|
await req("notice/SendList", "GET", "/api/notice/SendList?pageIndex=1&pageSize=10");
|
||||||
|
await req("notice/Statis", "GET", "/api/notice/Statis", { must: false });
|
||||||
|
|
||||||
|
const deptCode = Array.isArray(depts) && depts[0] ? depts[0].CodeID : "D002";
|
||||||
|
const receiveList = Array.isArray(buildings) && buildings[0] ? String(buildings[0].buildingId * 10000) : "ALL";
|
||||||
|
// 4b. 通知写链路:Create(草稿) → Edit 改标题 → GetDetail 校验 → Delete 自清理
|
||||||
|
if (WRITE && TEACHER) {
|
||||||
|
const nid = await req("notice/Create(draft)", "POST", "/api/notice/Create", {
|
||||||
|
body: { title: "【冒烟】通知A", content: "<p>smoke</p>", noticeType: "其他",
|
||||||
|
level: "normal", receiptMode: "none", deptId: deptCode, receiveType: "room",
|
||||||
|
receiveList, draft: true },
|
||||||
|
});
|
||||||
|
if (nid) {
|
||||||
|
await req("notice/Edit(改标题)", "POST", "/api/notice/Edit", { body: { id: nid, title: "【冒烟】通知B", receiptMode: "none" } });
|
||||||
|
const d = await req("notice/GetDetail(校验)", "GET", `/api/notice/GetDetail?serid=${nid}`);
|
||||||
|
push("notice/Edit 生效(标题→B)", !!(d && /通知B/.test(String(d.title || ""))), `title=${d && d.title}`, false);
|
||||||
|
await req("notice/Delete(cleanup)", "POST", "/api/notice/Delete", { body: [nid] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 家长端(学生 Cookie)
|
||||||
|
await req("notice/List(parent)", "GET", "/api/notice/List?pageIndex=1&pageSize=20", { cookie: STU, must: false });
|
||||||
|
await req("notice/MyNotices(parent)", "GET", "/api/notice/MyNotices", { cookie: STU, must: false });
|
||||||
|
|
||||||
|
// 6. 写链路自清理(用一次性数据,跑完即删;设 WRITE=0 跳过)
|
||||||
|
if (WRITE && TEACHER) {
|
||||||
|
// 6.1 字典:Add(noticeType) → ManageList 查得 → Delete
|
||||||
|
const tn = "冒烟类型" + Date.now();
|
||||||
|
await req("dict/Add(noticeType)", "POST", "/api/dict/Add", { form: { name: tn, type: "noticeType" } });
|
||||||
|
const ml = await req("dict/ManageList(noticeType)", "GET", "/api/dict/ManageList?type=noticeType", { must: false });
|
||||||
|
const dictItem = Array.isArray(ml) ? ml.find(x => x.Name === tn) : null;
|
||||||
|
push("dict/Add→ManageList 可查得", !!dictItem, dictItem ? "SerID=" + dictItem.SerID : "未找到新增项", false);
|
||||||
|
if (dictItem) await req("dict/Delete(cleanup)", "POST", "/api/dict/Delete", { form: { serid: dictItem.SerID, type: "noticeType" } });
|
||||||
|
|
||||||
|
// 6.2 联系组:AddGroup → GetGroup 查得 → DelGroup
|
||||||
|
const gn = "冒烟组" + Date.now();
|
||||||
|
await req("contacts/AddGroup", "POST", "/api/contacts/AddGroup", { form: { name: gn } });
|
||||||
|
const gp = await req("contacts/GetGroup", "GET", "/api/contacts/GetGroup?page=1&count=200", { must: false });
|
||||||
|
const grp = gp && Array.isArray(gp.list) ? gp.list.find(x => x.GroupName === gn) : null;
|
||||||
|
push("contacts/AddGroup→GetGroup 可查得", !!grp, grp ? "serid=" + grp.serid : "未找到", false);
|
||||||
|
if (grp) await req("contacts/DelGroup(cleanup)", "POST", "/api/contacts/DelGroup", { form: { ids: grp.serid } });
|
||||||
|
|
||||||
|
// 6.3 宿舍:AddBuilding → BuildingList 查得 → AddRoom → RoomList 查得 → DelRoom → DelBuilding
|
||||||
|
const bn = "冒烟楼" + Date.now();
|
||||||
|
await req("dorm/AddBuilding", "POST", "/api/dorm/AddBuilding", { form: { buildingName: bn, gender: "混合", floorCount: 1, roomsPerFloor: 1, bedsPerRoom: 2 } });
|
||||||
|
const bl = await req("dorm/BuildingList", "GET", "/api/dorm/BuildingList", { must: false });
|
||||||
|
const b = Array.isArray(bl) ? bl.find(x => x.buildingName === bn) : null;
|
||||||
|
push("dorm/AddBuilding→BuildingList 可查得", !!b, b ? "buildingId=" + b.buildingId : "未找到", false);
|
||||||
|
if (b) {
|
||||||
|
await req("dorm/AddRoom", "POST", "/api/dorm/AddRoom", { form: { buildingId: b.buildingId, floor: 1, roomNo: "901", bedCount: 2 } });
|
||||||
|
const rl = await req("dorm/RoomList", "GET", "/api/dorm/RoomList?buildingId=" + b.buildingId, { must: false });
|
||||||
|
const room = Array.isArray(rl) ? rl.find(x => x.roomNo === "901") : null;
|
||||||
|
push("dorm/AddRoom→RoomList 可查得", !!room, room ? "roomId=" + room.roomId : "未找到", false);
|
||||||
|
if (room) await req("dorm/DelRoom(cleanup)", "POST", "/api/dorm/DelRoom", { form: { roomId: room.roomId } });
|
||||||
|
await req("dorm/DelBuilding(cleanup)", "POST", "/api/dorm/DelBuilding", { form: { buildingId: b.buildingId } });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("SKIP 写链路(WRITE=0 或缺 TEACHER_COOKIE)");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== 汇总 ===");
|
||||||
|
const pass = results.filter(r => r.ok).length;
|
||||||
|
console.log(`${pass}/${results.length} 通过;必过失败 ${failed} 项`);
|
||||||
|
process.exit(failed ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => { console.error("脚本异常:", e); process.exit(2); });
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yunhai.dormnotice;
|
||||||
|
|
||||||
|
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.dormnotice")
|
||||||
|
@EnableTransactionManagement
|
||||||
|
public class DormNoticeApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 开启 fastjson 安全模式
|
||||||
|
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||||
|
SpringApplication.run(DormNoticeApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.dormnotice.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注在控制器方法上,声明一次需要落审计日志的操作(决策 D2「保留日志表」→ 统一操作日志)。
|
||||||
|
* <p>由 {@link com.yunhai.dormnotice.aspect.OperationLogAspect} 环绕拦截,写入 {@code sys_operation_log}。</p>
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface OperationLog {
|
||||||
|
|
||||||
|
/** 业务模块,如 "宿舍通知" */
|
||||||
|
String module() default "";
|
||||||
|
|
||||||
|
/** 操作类型:CREATE / UPDATE / DELETE / QUERY(缺省按 HTTP 方法推断) */
|
||||||
|
String type() default "";
|
||||||
|
|
||||||
|
/** 日志级别:1 重要(删除) / 2 次要(新增/编辑) / 3 一般(查询)。缺省按 type 推断 */
|
||||||
|
int level() default 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package com.yunhai.dormnotice.aspect;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.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.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;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把基础平台的当前登录用户({@link BoaUser})复制到自有 {@link UserContext}(ThreadLocal),请求结束清理。
|
||||||
|
* <p>参照 UserclassService 的 ControllerUserContextAspect:优先从控制器入参里的 {@link BoaUser} 取,
|
||||||
|
* 否则回退到框架 {@link BoaUserContext#get()}。端分离视角 role 取会话属性(SetRole 写入)。</p>
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(1)
|
||||||
|
public class ControllerUserContextAspect {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ControllerUserContextAspect.class);
|
||||||
|
|
||||||
|
/** 会话中记录的家长/教师视角键(SetRole 写、本切面读)。 */
|
||||||
|
public static final String SESSION_ROLE_KEY = "dorm_role";
|
||||||
|
|
||||||
|
@Around("execution(* com.yunhai.dormnotice.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.role = currentViewRole();
|
||||||
|
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 String currentViewRole() {
|
||||||
|
HttpSession session = session(false);
|
||||||
|
if (session != null) {
|
||||||
|
Object r = session.getAttribute(SESSION_ROLE_KEY);
|
||||||
|
if (UserContext.ROLE_PARENT.equals(r)) {
|
||||||
|
return UserContext.ROLE_PARENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UserContext.ROLE_TEACHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpSession session(boolean create) {
|
||||||
|
try {
|
||||||
|
HttpServletRequest req = request();
|
||||||
|
return req == null ? null : req.getSession(create);
|
||||||
|
} catch (Exception e) {
|
||||||
|
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,154 @@
|
|||||||
|
package com.yunhai.dormnotice.aspect;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.annotation.OperationLog;
|
||||||
|
import com.yunhai.dormnotice.entity.SysOperationLog;
|
||||||
|
import com.yunhai.dormnotice.repository.SysOperationLogRepository;
|
||||||
|
import com.yunhai.dormnotice.utils.UserContext;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一操作日志切面:环绕 {@link OperationLog} 标注的方法,落 {@code sys_operation_log}。
|
||||||
|
* <p>@Order 大于 {@link ControllerUserContextAspect},保证写日志时 {@link UserContext} 已填充。
|
||||||
|
* 落库异常仅告警、不外抛,绝不因记日志影响主业务。</p>
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(10)
|
||||||
|
public class OperationLogAspect {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OperationLogAspect.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysOperationLogRepository sysOperationLogRepository;
|
||||||
|
|
||||||
|
@Pointcut("@annotation(com.yunhai.dormnotice.annotation.OperationLog)")
|
||||||
|
public void logPoint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("logPoint()")
|
||||||
|
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
int status = 0;
|
||||||
|
String errorMsg = null;
|
||||||
|
try {
|
||||||
|
return pjp.proceed();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
status = 1;
|
||||||
|
errorMsg = truncate(t.getMessage(), 2000);
|
||||||
|
throw t;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
save(pjp, status, errorMsg, System.currentTimeMillis() - start);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[OperationLog] 写入操作日志失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save(ProceedingJoinPoint pjp, int status, String errorMsg, long cost) {
|
||||||
|
SysOperationLog entity = new SysOperationLog();
|
||||||
|
MethodSignature sig = (MethodSignature) pjp.getSignature();
|
||||||
|
OperationLog ann = sig.getMethod().getAnnotation(OperationLog.class);
|
||||||
|
entity.setClassMethod(sig.getDeclaringType().getSimpleName() + "#" + sig.getName());
|
||||||
|
|
||||||
|
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() : inferType(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);
|
||||||
|
sysOperationLogRepository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inferType(String httpMethod) {
|
||||||
|
if (httpMethod == null) {
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
switch (httpMethod.toUpperCase()) {
|
||||||
|
case "POST":
|
||||||
|
return "CREATE";
|
||||||
|
case "PUT":
|
||||||
|
return "UPDATE";
|
||||||
|
case "DELETE":
|
||||||
|
return "DELETE";
|
||||||
|
default:
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String s;
|
||||||
|
try {
|
||||||
|
s = a.getClass().isArray() ? Arrays.deepToString((Object[]) a) : String.valueOf(a);
|
||||||
|
} catch (Exception e) {
|
||||||
|
s = "[unprintable]";
|
||||||
|
}
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append(s);
|
||||||
|
}
|
||||||
|
String out = sb.toString();
|
||||||
|
out = out.replaceAll("(?i)(password|pwd|secret|token)=([^&]*)", "$1=***");
|
||||||
|
return truncate(out, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.dormnotice.common.listener;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.BaseEntity;
|
||||||
|
import com.yunhai.platform.auth.LoginUser;
|
||||||
|
import com.yunhai.platform.auth.UserContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.persistence.PrePersist;
|
||||||
|
import javax.persistence.PreUpdate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(平台当前登录人)与时间戳。
|
||||||
|
* 对应 {@link BaseEntity} 的 createBy / updateBy / createTime / updateTime 字段。
|
||||||
|
* 当前登录人取自基础平台 {@link UserContextHolder}。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class NoticeAuditListener {
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist(BaseEntity entity) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
entity.setCreateTime(now);
|
||||||
|
entity.setUpdateTime(now);
|
||||||
|
String account = currentAccount();
|
||||||
|
if (account != null) {
|
||||||
|
if (entity.getCreateBy() == null) {
|
||||||
|
entity.setCreateBy(account);
|
||||||
|
}
|
||||||
|
entity.setUpdateBy(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate(BaseEntity entity) {
|
||||||
|
entity.setUpdateTime(LocalDateTime.now());
|
||||||
|
String account = currentAccount();
|
||||||
|
if (account != null) {
|
||||||
|
entity.setUpdateBy(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentAccount() {
|
||||||
|
LoginUser user = UserContextHolder.get();
|
||||||
|
return user == null ? null : user.getLoginName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yunhai.dormnotice.common.validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||||
|
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||||
|
*/
|
||||||
|
public interface ValidationGroups {
|
||||||
|
interface Create {}
|
||||||
|
interface Update {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.yunhai.dormnotice.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,70 @@
|
|||||||
|
package com.yunhai.dormnotice.config;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.SysDict;
|
||||||
|
import com.yunhai.dormnotice.repository.SysDictRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典自举种子:启动时若 sys_dict 缺 dept / noticeType 则补入默认口径(幂等,仅在为空时插)。
|
||||||
|
* <p>解决「发布部门/通知类型下拉为空」——不依赖 DBA 手动跑 dormnotice_init.sql。
|
||||||
|
* 生产可用 {@code dormnotice.seed-dict=false} 关闭。</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DictSeedRunner implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DictSeedRunner.class);
|
||||||
|
|
||||||
|
private static final String[][] DEPTS = {
|
||||||
|
{"D001", "校办"}, {"D002", "德育处"}, {"D003", "生活管理科"}, {"D004", "教务处"},
|
||||||
|
{"D005", "后勤保障部"}, {"D006", "校医务室"}, {"D007", "武术训练部"}, {"D008", "安全管理办公室"},
|
||||||
|
};
|
||||||
|
private static final String[][] NOTICE_TYPES = {
|
||||||
|
{"T001", "作息调整"}, {"T002", "宿舍调整"}, {"T003", "卫生检查"}, {"T004", "违规通报"},
|
||||||
|
{"T005", "停水停电"}, {"T006", "安全排查"}, {"T007", "留宿申请"}, {"T008", "家长探视"},
|
||||||
|
{"T009", "夜间查寝"}, {"T010", "物品清退"}, {"T011", "疫情防控"}, {"T012", "假期安排"}, {"T013", "其他"},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysDictRepository sysDictRepository;
|
||||||
|
|
||||||
|
@Value("${dormnotice.seed-dict:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seed("dept", DEPTS);
|
||||||
|
seed("noticeType", NOTICE_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seed(String type, String[][] items) {
|
||||||
|
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(type);
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
log.info("[DictSeed] 字典 {} 已有 {} 条,跳过种子", type, existing.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int order = 1;
|
||||||
|
for (String[] it : items) {
|
||||||
|
SysDict d = new SysDict();
|
||||||
|
d.setDictType(type);
|
||||||
|
d.setDictKey(it[0]);
|
||||||
|
d.setDictValue(it[1]);
|
||||||
|
d.setSortOrder(order++);
|
||||||
|
d.setEnableFlag(1);
|
||||||
|
sysDictRepository.save(d);
|
||||||
|
}
|
||||||
|
log.info("[DictSeed] 字典 {} 已种入 {} 条", type, items.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.dormnotice.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,67 @@
|
|||||||
|
package com.yunhai.dormnotice.config;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormNoticeRead;
|
||||||
|
import com.yunhai.dormnotice.entity.Receipt;
|
||||||
|
import com.yunhai.dormnotice.repository.DormNoticeReadRepository;
|
||||||
|
import com.yunhai.dormnotice.repository.ReceiptRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史阅读记录回填(幂等):把已确认/已填报的学生补写进 dorm_notice_read。
|
||||||
|
* <p>「已读人数」改为按学生级阅读记录统计后,旧数据(在读日志上线前打开/确认的)没有记录,
|
||||||
|
* 会出现「已读 0 但已回执 N」的矛盾。确认/填报必然先打开,故据回执回填阅读记录,使 已读 ≥ 已回执。
|
||||||
|
* 不回填 receipt.read=true(那是旧的整房标记、被床位放大,回填会重现虚高)。</p>
|
||||||
|
* 可用 {@code dormnotice.backfill-read=false} 关闭。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Order(20)
|
||||||
|
public class ReadBackfillRunner implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ReadBackfillRunner.class);
|
||||||
|
|
||||||
|
@Autowired private ReceiptRepository receiptRepository;
|
||||||
|
@Autowired private DormNoticeReadRepository readRepository;
|
||||||
|
|
||||||
|
@Value("${dormnotice.backfill-read:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Receipt> acted = receiptRepository.findByStatusIn(java.util.Arrays.asList("confirmed", "filled"));
|
||||||
|
int added = 0;
|
||||||
|
for (Receipt r : acted) {
|
||||||
|
if (r.getNoticeId() == null || r.getStudentId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (readRepository.existsByNoticeIdAndStudentId(r.getNoticeId(), r.getStudentId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DormNoticeRead rd = new DormNoticeRead();
|
||||||
|
rd.setNoticeId(r.getNoticeId());
|
||||||
|
rd.setStudentId(r.getStudentId());
|
||||||
|
rd.setRoomId(r.getRoomId());
|
||||||
|
rd.setReadAt(r.getSubmittedAt() != null ? r.getSubmittedAt() : LocalDateTime.now());
|
||||||
|
rd.setSchId(r.getSchId());
|
||||||
|
readRepository.save(rd);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
if (added > 0) {
|
||||||
|
log.info("[ReadBackfill] 依据已确认/已填报回执回填阅读记录 {} 条", added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.dormnotice.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,替代旧 Springfox)。
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI noticeOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("通知公告接口文档")
|
||||||
|
.description("通知公告子应用:发通知、列表、删除、编辑、管理员查看、统计(springdoc-openapi)")
|
||||||
|
.version("1.0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.yunhai.dormnotice.constant;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍通知枚举文案 + 统一回执单模板(系统预置常量,落库不落表;决策 D3)。
|
||||||
|
* 字段结构与前端 types/dorm.ts 的 FillField / ReceiptFormTemplate 一一对齐。
|
||||||
|
*/
|
||||||
|
public final class DormEnums {
|
||||||
|
|
||||||
|
private DormEnums() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 文案映射 ============
|
||||||
|
public static final Map<String, String> NOTICE_STATUS_DESC = desc("draft", "草稿", "published", "已发布", "closed", "已截止", "finished", "已结束");
|
||||||
|
public static final Map<String, String> RECEIPT_MODE_DESC = desc("none", "仅通知", "confirm", "需确认", "fill", "需填报");
|
||||||
|
public static final Map<String, String> LEVEL_DESC = desc("normal", "普通", "important", "重要", "urgent", "紧急");
|
||||||
|
public static final Map<String, String> RECEIPT_STATUS_DESC = desc("none", "未回执", "confirmed", "已确认", "filled", "已填报", "canceled", "已取消");
|
||||||
|
public static final Map<String, String> FORM_TYPE_DESC = desc("stay", "留宿申请单", "visit", "探视预约单", "adjust", "调宿申请单", "general", "通用回执单");
|
||||||
|
|
||||||
|
private static Map<String, String> desc(String... kv) {
|
||||||
|
Map<String, String> m = new LinkedHashMap<>();
|
||||||
|
for (int i = 0; i + 1 < kv.length; i += 2) {
|
||||||
|
m.put(kv[i], kv[i + 1]);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String statusDesc(String v) { return NOTICE_STATUS_DESC.getOrDefault(v, "已发布"); }
|
||||||
|
public static String modeDesc(String v) { return RECEIPT_MODE_DESC.getOrDefault(v, "仅通知"); }
|
||||||
|
public static String levelDesc(String v) { return LEVEL_DESC.getOrDefault(v, "普通"); }
|
||||||
|
public static String receiptStatusDesc(String v) { return RECEIPT_STATUS_DESC.getOrDefault(v, v); }
|
||||||
|
public static String formTypeDesc(String v) { return FORM_TYPE_DESC.getOrDefault(v, "通用回执单"); }
|
||||||
|
|
||||||
|
// ============ 公共区字段(所有单据一致) ============
|
||||||
|
private static Map<String, Object> commonConclusion() {
|
||||||
|
Map<String, Object> f = new LinkedHashMap<>();
|
||||||
|
f.put("key", "conclusion");
|
||||||
|
f.put("label", "回执结论");
|
||||||
|
f.put("type", "select");
|
||||||
|
f.put("required", true);
|
||||||
|
f.put("group", "common");
|
||||||
|
f.put("options", options(
|
||||||
|
opt("同意 / 确认办理", "同意"),
|
||||||
|
opt("不同意 / 不参与", "不同意"),
|
||||||
|
opt("需要电话沟通", "需沟通")));
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 专属区字段(按单据类型) ============
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static List<Map<String, Object>> buildFormFields(String formType) {
|
||||||
|
List<Map<String, Object>> fields = new ArrayList<>();
|
||||||
|
fields.add(commonConclusion());
|
||||||
|
fields.addAll(specificFields(formType));
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> specificFields(String formType) {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
switch (formType == null ? "general" : formType) {
|
||||||
|
case "stay":
|
||||||
|
list.add(field("stayStart", "留宿开始日期", "date", true, "specific", "选择留宿开始日期", null, null));
|
||||||
|
list.add(field("stayEnd", "留宿结束日期", "date", true, "specific", "选择留宿结束日期", null, null));
|
||||||
|
list.add(field("stayCount", "留宿人数", "number", true, "specific", null, "人", null));
|
||||||
|
list.add(field("stayReason", "留宿事由", "select", true, "specific", null, null, options(
|
||||||
|
opt("家远不便往返", "家远不便往返"), opt("赛前集训", "赛前集训"),
|
||||||
|
opt("伤病康复观察", "伤病康复观察"), opt("学业补差", "学业补差"),
|
||||||
|
opt("其他(请在补充说明中写明)", "其他"))));
|
||||||
|
break;
|
||||||
|
case "visit":
|
||||||
|
list.add(field("visitDate", "到访日期", "date", true, "specific", "选择到访日期", null, null));
|
||||||
|
list.add(field("visitSlot", "到访时段", "select", true, "specific", null, null, options(
|
||||||
|
opt("上午 09:00-11:30", "上午 09:00-11:30"), opt("下午 14:00-17:00", "下午 14:00-17:00"),
|
||||||
|
opt("晚间 18:30-20:00", "晚间 18:30-20:00"))));
|
||||||
|
list.add(field("visitorCount", "到访人数", "number", true, "specific", null, "人", null));
|
||||||
|
break;
|
||||||
|
case "adjust":
|
||||||
|
list.add(field("moveDate", "可配合搬迁日期", "date", true, "specific", "选择可配合搬迁的日期", null, null));
|
||||||
|
list.add(field("adjustReason", "情况说明", "textarea", false, "specific",
|
||||||
|
"如孩子有腰伤需住下铺、需与同训练组同住、恐高等,请在此说明", null, null));
|
||||||
|
break;
|
||||||
|
case "general":
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单据模板列表(发布页选型用)。公共区由 buildFormFields 统一提供,此处只列专属区。 */
|
||||||
|
public static List<Map<String, Object>> formTemplates() {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
list.add(template("stay", "留宿申请单", "周末 / 假期学生留宿申请,需注明留宿起止与事由", specificFields("stay")));
|
||||||
|
list.add(template("visit", "探视预约单", "家长到校探视预约,需注明到访日期、时段与人数", specificFields("visit")));
|
||||||
|
list.add(template("adjust", "调宿申请单", "床位调整 / 宿舍调换确认,需注明可配合搬迁的日期", specificFields("adjust")));
|
||||||
|
list.add(template("general", "通用回执单", "只需家长给出回执结论并说明情况,无额外登记项", specificFields("general")));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> formTypes() {
|
||||||
|
return Arrays.asList("stay", "visit", "adjust", "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 构造小工具 ============
|
||||||
|
private static Map<String, Object> template(String type, String name, String desc, List<Map<String, Object>> fields) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("type", type);
|
||||||
|
m.put("name", name);
|
||||||
|
m.put("desc", desc);
|
||||||
|
m.put("fields", fields);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> field(String key, String label, String type, boolean required,
|
||||||
|
String group, String placeholder, String unit, List<Map<String, Object>> options) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("key", key);
|
||||||
|
m.put("label", label);
|
||||||
|
m.put("type", type);
|
||||||
|
m.put("required", required);
|
||||||
|
m.put("group", group);
|
||||||
|
if (placeholder != null) m.put("placeholder", placeholder);
|
||||||
|
if (unit != null) m.put("unit", unit);
|
||||||
|
if (options != null) m.put("options", options);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> options(Map<String, Object>... opts) {
|
||||||
|
return new ArrayList<>(Arrays.asList(opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> opt(String label, String value) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("label", label);
|
||||||
|
m.put("value", value);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.aspect.ControllerUserContextAspect;
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.dormnotice.service.IDormService;
|
||||||
|
import com.yunhai.dormnotice.service.IFileService;
|
||||||
|
import com.yunhai.dormnotice.service.IUserCenterService;
|
||||||
|
import com.yunhai.platform.auth.LoginUser;
|
||||||
|
import com.yunhai.platform.auth.UserContextHolder;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台代理域接口(base/)。
|
||||||
|
* <p>路径前缀:{@code Config.API_PATH + "/base/"}(原硬编码已修正)。
|
||||||
|
* 承载平台代理接口:GetUserInfo / GetUserMod / GetSchoolAll / GetGroupUser / Logout,
|
||||||
|
* 业务逻辑已下沉 {@link IUserCenterService},本控制器仅做"取参 → 调 Service → 返回"。
|
||||||
|
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||||
|
public class BaseApiController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BaseApiController.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUserCenterService userCenterService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFileService fileService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDormService dormService;
|
||||||
|
|
||||||
|
// ============================ FileUpload ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件上传(单文件)。
|
||||||
|
* 前端: POST base/FileUpload (form-data: file)
|
||||||
|
* 返回: { fileid, host, fileurl, name, type, size }
|
||||||
|
*/
|
||||||
|
@PostMapping("FileUpload")
|
||||||
|
public UpFileResultVo fileUpload(@RequestParam("file") MultipartFile file) {
|
||||||
|
return fileService.uploadToResult(file, "dormnotice");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetUserInfo ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息(CAS 会话注入)。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetUserInfo")
|
||||||
|
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||||
|
return authUserWithDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetUserMod ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户已授权的模块列表(左侧菜单树)。
|
||||||
|
* 模块树组装已下沉 IUserCenterService。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetUserMod")
|
||||||
|
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
|
||||||
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
return userCenterService.getUserModuleTree(authUserWithDept.getId(), schId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetSchoolAll ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户学校组及用户(树形结构)。
|
||||||
|
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetSchoolAll")
|
||||||
|
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
|
||||||
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
return userCenterService.getSchoolAllTree(schId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetGroupUser ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定组内用户(教师 + 管理员)。
|
||||||
|
* 空桩已改为显式报错(避免前端误以为组内无用户),由 IUserCenterService 统一处理。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetGroupUser")
|
||||||
|
public List<Map<String, Object>> getGroupUser(
|
||||||
|
@RequestParam("gid") Integer gid,
|
||||||
|
@RequestParam(value = "role", defaultValue = "2") Integer role) {
|
||||||
|
return userCenterService.getGroupUser(gid, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetDormTree ============================
|
||||||
|
|
||||||
|
/** 楼栋→楼层→房间树(宿舍通知发布对象,选到房间一级)。 */
|
||||||
|
@GetMapping("GetDormTree")
|
||||||
|
public List<Map<String, Object>> getDormTree() {
|
||||||
|
return dormService.dormTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ SetRole(端分离视角) ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换当前视角(teacher 发布端 / parent 家长端),落会话供 {@link ControllerUserContextAspect} 读取。
|
||||||
|
* 家长即登录学生账号,不单列。
|
||||||
|
*/
|
||||||
|
@PostMapping("SetRole")
|
||||||
|
public Map<String, Object> setRole(@RequestBody Map<String, Object> body, HttpSession session) {
|
||||||
|
String as = body == null ? null : String.valueOf(body.get("as"));
|
||||||
|
String role = com.yunhai.dormnotice.utils.UserContext.ROLE_PARENT.equals(as)
|
||||||
|
? com.yunhai.dormnotice.utils.UserContext.ROLE_PARENT
|
||||||
|
: com.yunhai.dormnotice.utils.UserContext.ROLE_TEACHER;
|
||||||
|
session.setAttribute(ControllerUserContextAspect.SESSION_ROLE_KEY, role);
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
r.put("role", role);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Logout ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录。
|
||||||
|
* 认证由平台网关处理,此处仅返回成功。
|
||||||
|
*/
|
||||||
|
@PostMapping("Logout")
|
||||||
|
public void logout() {
|
||||||
|
LoginUser user = UserContextHolder.get();
|
||||||
|
log.info("[logout] 用户退出登录: {}", user == null ? null : user.getLoginName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.service.IContactsService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系组接口(contacts/)。
|
||||||
|
* <p>路径前缀:{@code Config.API_PATH + "/contacts/"}(原硬编码已修正为 Config 拼接)。
|
||||||
|
* 方法名已为 PascalCase 且写操作已是 POST,保持不动。
|
||||||
|
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/contacts/")
|
||||||
|
public class ContactsController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IContactsService contactsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加联系组。
|
||||||
|
*/
|
||||||
|
@PostMapping("AddGroup")
|
||||||
|
public void addGroup(
|
||||||
|
@RequestParam("name") String name,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
|
||||||
|
contactsService.addGroup(name, Integer.parseInt(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组列表。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetGroup")
|
||||||
|
public Map<String, Object> getGroup(
|
||||||
|
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||||
|
@RequestParam(value = "count", defaultValue = "0") Integer count,
|
||||||
|
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||||
|
return contactsService.getGroup(page, count, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑联系组。
|
||||||
|
*/
|
||||||
|
@PostMapping("EditGroup")
|
||||||
|
public void editGroup(
|
||||||
|
@RequestParam("name") String name,
|
||||||
|
@RequestParam("id") String id,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
|
||||||
|
contactsService.editGroup(Integer.parseInt(id), name, Integer.parseInt(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除联系组(支持批量)。
|
||||||
|
*/
|
||||||
|
@PostMapping("DelGroup")
|
||||||
|
public void delGroup(@RequestParam("ids") String ids) {
|
||||||
|
contactsService.delGroup(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置联系组教师。
|
||||||
|
*/
|
||||||
|
@PostMapping("SetContactsTea")
|
||||||
|
public void setContactsTea(
|
||||||
|
@RequestParam("gid") String gid,
|
||||||
|
@RequestParam(value = "ids", required = false) String ids) {
|
||||||
|
contactsService.setContactsTea(Integer.parseInt(gid), ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组教师列表。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetContactsTea")
|
||||||
|
public Map<String, Object> getContactsTea(
|
||||||
|
@RequestParam("gid") String gid,
|
||||||
|
@RequestParam(value = "page", defaultValue = "0") Integer page,
|
||||||
|
@RequestParam(value = "count", defaultValue = "0") Integer count) {
|
||||||
|
return contactsService.getContactsTea(gid, page, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组树。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetGroupTree")
|
||||||
|
public List<Map<String, Object>> getGroupTree(@RequestParam("type") Integer type) {
|
||||||
|
return contactsService.getGroupTree(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门角色组树。
|
||||||
|
*/
|
||||||
|
@GetMapping("GetDeptRoleGroupTree")
|
||||||
|
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||||
|
return contactsService.getDeptRoleGroupTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.annotation.OperationLog;
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.service.IDictService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统字典接口。@RequestParam 表单类接口(前端 postForm);List/ManageList/Sync 为 GET。
|
||||||
|
* type 维度:dept / noticeType(缺省 noticeType)。入参名大小写严格对齐前端 dorm.ts。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/dict/")
|
||||||
|
public class DictController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDictService dictService;
|
||||||
|
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<Map<String, Object>> list(@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
return dictService.list(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("ManageList")
|
||||||
|
public List<Map<String, Object>> manageList(@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
return dictService.manageList(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Add")
|
||||||
|
@OperationLog(module = "字典", type = "CREATE", level = 2)
|
||||||
|
public void add(@RequestParam("name") String name,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
dictService.add(name, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Update")
|
||||||
|
@OperationLog(module = "字典", type = "UPDATE", level = 2)
|
||||||
|
public void update(@RequestParam("SerID") Integer serId,
|
||||||
|
@RequestParam(value = "Name", required = false) String name,
|
||||||
|
@RequestParam(value = "Enable", required = false) Integer enable,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
dictService.update(serId, name, enable, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "字典", type = "DELETE", level = 1)
|
||||||
|
public void delete(@RequestParam("serid") Integer serId,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
dictService.delete(serId, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Sort")
|
||||||
|
public void sort(@RequestParam("ids") String ids,
|
||||||
|
@RequestParam(value = "type", required = false, defaultValue = "noticeType") String type) {
|
||||||
|
dictService.sort(ids, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("Sync")
|
||||||
|
public Boolean sync() {
|
||||||
|
return dictService.isSmsEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.annotation.OperationLog;
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.service.IDormService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍楼栋 / 房间 / 床位接口(/api/dorm/*)。写接口对齐前端 postForm(@RequestParam)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/dorm/")
|
||||||
|
public class DormController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IDormService dormService;
|
||||||
|
|
||||||
|
@GetMapping("BuildingList")
|
||||||
|
public List<Map<String, Object>> buildingList() {
|
||||||
|
return dormService.buildingList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("RoomList")
|
||||||
|
public List<Map<String, Object>> roomList(@RequestParam(required = false) Integer buildingId,
|
||||||
|
@RequestParam(required = false) String keyword) {
|
||||||
|
return dormService.roomList(buildingId, keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("AddBuilding")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "CREATE", level = 2)
|
||||||
|
public void addBuilding(@RequestParam Map<String, String> data) {
|
||||||
|
dormService.addBuilding(toObj(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("EditBuilding")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "UPDATE", level = 2)
|
||||||
|
public void editBuilding(@RequestParam Map<String, String> data) {
|
||||||
|
dormService.editBuilding(toObj(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("DelBuilding")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "DELETE", level = 1)
|
||||||
|
public void delBuilding(@RequestParam Integer buildingId) {
|
||||||
|
dormService.delBuilding(buildingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("AddRoom")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "CREATE", level = 2)
|
||||||
|
public void addRoom(@RequestParam Map<String, String> data) {
|
||||||
|
dormService.addRoom(toObj(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("EditRoom")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "UPDATE", level = 2)
|
||||||
|
public void editRoom(@RequestParam Map<String, String> data) {
|
||||||
|
dormService.editRoom(toObj(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("DelRoom")
|
||||||
|
@OperationLog(module = "宿舍设置", type = "DELETE", level = 1)
|
||||||
|
public void delRoom(@RequestParam Integer roomId) {
|
||||||
|
dormService.delRoom(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("RoomStudents")
|
||||||
|
public List<Map<String, Object>> roomStudents(@RequestParam Integer roomId) {
|
||||||
|
return dormService.roomStudents(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("PendingStudents")
|
||||||
|
public List<Map<String, Object>> pendingStudents(@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String gender) {
|
||||||
|
return dormService.pendingStudents(keyword, gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("AssignStudent")
|
||||||
|
public void assignStudent(@RequestParam Integer roomId, @RequestParam String studentIds) {
|
||||||
|
List<Integer> ids = new ArrayList<>();
|
||||||
|
for (String s : studentIds.split(",")) {
|
||||||
|
if (!s.trim().isEmpty()) {
|
||||||
|
ids.add(Integer.parseInt(s.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dormService.assignStudent(roomId, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("RemoveStudent")
|
||||||
|
public void removeStudent(@RequestParam Integer roomId, @RequestParam Integer studentId) {
|
||||||
|
dormService.removeStudent(roomId, studentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("TransferStudent")
|
||||||
|
public Map<String, Object> transferStudent(@RequestParam Integer studentId,
|
||||||
|
@RequestParam Integer fromRoomId,
|
||||||
|
@RequestParam Integer toRoomId) {
|
||||||
|
return dormService.transferStudent(studentId, fromRoomId, toRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("SetMonitor")
|
||||||
|
public void setMonitor(@RequestParam Integer roomId, @RequestParam Integer studentId) {
|
||||||
|
dormService.setMonitor(roomId, studentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 bschclass 学籍导入全校学生到待安排池(幂等),返回新增人数。 */
|
||||||
|
@PostMapping("ImportStudents")
|
||||||
|
public Map<String, Object> importStudents() {
|
||||||
|
Map<String, Object> r = new java.util.LinkedHashMap<>();
|
||||||
|
r.put("added", dormService.importStudentsFromBschclass());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> toObj(Map<String, String> src) {
|
||||||
|
return src.entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.dormnotice.service.IFileService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件域接口(file/)。
|
||||||
|
* <p>路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。
|
||||||
|
* 方法名统一 PascalCase;写操作一律 POST。
|
||||||
|
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/file/")
|
||||||
|
public class FileController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFileService fileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件上传(单文件)。
|
||||||
|
* 前端: POST file/Upload (form-data)
|
||||||
|
* 返回: { fileid, host, fileurl, name, type, size }
|
||||||
|
*/
|
||||||
|
@PostMapping("Upload")
|
||||||
|
public UpFileResultVo upload(@RequestParam("file") MultipartFile file) {
|
||||||
|
return fileService.uploadToResult(file, "dormnotice");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片上传(多文件,返回数组)。
|
||||||
|
* 前端: POST file/UploadImage (form-data, accept=image/*)
|
||||||
|
*/
|
||||||
|
@PostMapping("UploadImage")
|
||||||
|
public List<UpFileVo> uploadImage(@RequestParam("file") MultipartFile[] files) {
|
||||||
|
return fileService.uploadImages(files, "dormnotice");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 文件下载(浏览器直接 location.href 跳转)。
|
||||||
|
*/
|
||||||
|
@GetMapping("Down")
|
||||||
|
public void down(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||||
|
fileService.download(id, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件在线预览(inline 输出,前端取 Blob 交给 open-file-viewer 渲染)。
|
||||||
|
*/
|
||||||
|
@GetMapping("Preview")
|
||||||
|
public void preview(@RequestParam("id") Integer id,
|
||||||
|
@RequestParam(value = "type", required = false) Integer type,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
fileService.preview(id, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件。
|
||||||
|
*/
|
||||||
|
@PostMapping("Delete")
|
||||||
|
public void delete(@RequestParam("fileId") Integer fileId) {
|
||||||
|
fileService.delete(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某通知下的附件列表。
|
||||||
|
*/
|
||||||
|
@GetMapping("List")
|
||||||
|
public List<UpFileVo> list(@RequestParam("topicId") Integer topicId) {
|
||||||
|
return fileService.listByTopic("dormnotice", topicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量绑定附件到通知。
|
||||||
|
*/
|
||||||
|
@PostMapping("Bind")
|
||||||
|
public void bind(@RequestParam("fileIds") List<Integer> fileIds,
|
||||||
|
@RequestParam("topicId") Integer topicId) {
|
||||||
|
fileService.bindToTopic(fileIds, topicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.yunhai.dormnotice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.annotation.OperationLog;
|
||||||
|
import com.yunhai.dormnotice.config.Config;
|
||||||
|
import com.yunhai.dormnotice.service.INoticeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍通知接口(/api/notice/*)。Create/Edit/Delete/SubmitReceipt/Review 走 @RequestBody(JSON),
|
||||||
|
* 其余读接口 GET、写接口 postForm(@RequestParam)。响应由平台统一包装 {code,msg,data}。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/notice/")
|
||||||
|
public class NoticeController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private INoticeService noticeService;
|
||||||
|
|
||||||
|
// ===== 列表 / 详情 =====
|
||||||
|
|
||||||
|
@GetMapping("List")
|
||||||
|
public Map<String, Object> list(@RequestParam Map<String, String> q) {
|
||||||
|
return noticeService.list(toObj(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("SendList")
|
||||||
|
public Map<String, Object> sendList(@RequestParam Map<String, String> q) {
|
||||||
|
return noticeService.sendList(toObj(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("Detail")
|
||||||
|
public Map<String, Object> detail(@RequestParam Integer serid, @RequestParam(required = false, defaultValue = "1") Integer type) {
|
||||||
|
return noticeService.detail(serid, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("GetDetail")
|
||||||
|
public Map<String, Object> getDetail(@RequestParam Integer serid) {
|
||||||
|
return noticeService.getDetail(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("GetNextPrev")
|
||||||
|
public Map<String, Object> nextPrev(@RequestParam Integer serid, @RequestParam(required = false) String type) {
|
||||||
|
return noticeService.nextPrev(serid, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("ReadDetail")
|
||||||
|
public List<Map<String, Object>> readDetail(@RequestParam Integer serid) {
|
||||||
|
return noticeService.readDetail(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("FormTemplates")
|
||||||
|
public List<Map<String, Object>> formTemplates() {
|
||||||
|
return noticeService.formTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 写入 =====
|
||||||
|
|
||||||
|
@PostMapping("Create")
|
||||||
|
@OperationLog(module = "宿舍通知", type = "CREATE", level = 2)
|
||||||
|
public Integer create(@RequestBody Map<String, Object> body) {
|
||||||
|
return noticeService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Edit")
|
||||||
|
@OperationLog(module = "宿舍通知", type = "UPDATE", level = 2)
|
||||||
|
public void edit(@RequestBody Map<String, Object> body) {
|
||||||
|
noticeService.edit(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Delete")
|
||||||
|
@OperationLog(module = "宿舍通知", type = "DELETE", level = 1)
|
||||||
|
public void delete(@RequestBody List<Integer> ids) {
|
||||||
|
noticeService.delete(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("UserPull")
|
||||||
|
public void userPull(@RequestParam Integer serid) {
|
||||||
|
noticeService.userPull(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("SetTopmost")
|
||||||
|
public void setTopmost(@RequestParam Integer serid, @RequestParam(required = false, defaultValue = "1") Integer topmost) {
|
||||||
|
noticeService.setTopmost(serid, topmost);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Expedite")
|
||||||
|
public Map<String, Object> expedite(@RequestParam Integer serid) {
|
||||||
|
return noticeService.expedite(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("ReceiveRooms")
|
||||||
|
public void receiveRooms(@RequestParam Integer serid, @RequestParam String rooms) {
|
||||||
|
noticeService.receiveRooms(serid, rooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 回执 =====
|
||||||
|
|
||||||
|
@GetMapping("Receipts")
|
||||||
|
public Map<String, Object> receipts(@RequestParam Integer serid, @RequestParam(required = false, defaultValue = "all") String status) {
|
||||||
|
return noticeService.receipts(serid, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("ReceiptDetail")
|
||||||
|
public Map<String, Object> receiptDetail(@RequestParam Integer receiptId) {
|
||||||
|
return noticeService.receiptDetail(receiptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Review")
|
||||||
|
public void review(@RequestBody Map<String, Object> body) {
|
||||||
|
noticeService.review(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("Remind")
|
||||||
|
public Map<String, Object> remind(@RequestParam Integer serid) {
|
||||||
|
return noticeService.remind(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("ExportReceipts")
|
||||||
|
public String exportReceipts(@RequestParam Integer serid) {
|
||||||
|
return noticeService.exportReceipts(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("Statis")
|
||||||
|
public Map<String, Object> statis(@RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) {
|
||||||
|
return noticeService.statis(startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 家长端 =====
|
||||||
|
|
||||||
|
@GetMapping("MyNotices")
|
||||||
|
public Map<String, Object> myNotices(@RequestParam Map<String, String> q) {
|
||||||
|
return noticeService.myNotices(toObj(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("MyNotice")
|
||||||
|
public Map<String, Object> myNotice(@RequestParam Integer serid) {
|
||||||
|
return noticeService.myNotice(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("SubmitReceipt")
|
||||||
|
public Map<String, Object> submitReceipt(@RequestBody Map<String, Object> body) {
|
||||||
|
return noticeService.submitReceipt(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("CancelReceipt")
|
||||||
|
public void cancelReceipt(@RequestParam Integer serid) {
|
||||||
|
noticeService.cancelReceipt(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("InitializeRed")
|
||||||
|
public Map<String, Object> initializeRed() {
|
||||||
|
return noticeService.initializeRed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("AllFileDown")
|
||||||
|
public void allFileDown(@RequestParam Integer dataid, javax.servlet.http.HttpServletResponse response) {
|
||||||
|
noticeService.allFileDown(dataid, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("MarkRead")
|
||||||
|
public void markRead(@RequestParam Integer serid, @RequestParam Integer studentId) {
|
||||||
|
noticeService.markRead(serid, studentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("RemindStudent")
|
||||||
|
public Map<String, Object> remindStudent(@RequestParam Integer serid, @RequestParam Integer studentId) {
|
||||||
|
return noticeService.remindStudent(serid, studentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> toObj(Map<String, String> src) {
|
||||||
|
return new HashMap<>(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.common.listener.NoticeAuditListener;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link NoticeAuditListener} 自动填充)。
|
||||||
|
*/
|
||||||
|
@MappedSuperclass
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(NoticeAuditListener.class)
|
||||||
|
public abstract class BaseEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人登录账号
|
||||||
|
*/
|
||||||
|
@Column(name = "create_by", updatable = false)
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@Column(name = "create_time", updatable = false)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人登录账号
|
||||||
|
*/
|
||||||
|
@Column(name = "update_by")
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@Column(name = "update_time")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系组。对应原 .NET Contacts_Group 表。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "contacts_group")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class ContactsGroup extends BaseEntity {
|
||||||
|
|
||||||
|
/** 组名称 */
|
||||||
|
@Column(name = "group_name", length = 200)
|
||||||
|
private String groupName;
|
||||||
|
|
||||||
|
/** 创建人用户ID(决策 D4:只存 id,不存账号) */
|
||||||
|
@Column(name = "creator_id")
|
||||||
|
private Integer creatorId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系组-用户关联。对应原 .NET Contacts_Tea 表。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "contacts_group_user",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_group_user",
|
||||||
|
columnNames = {"group_id", "user_id"}))
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class ContactsGroupUser extends BaseEntity {
|
||||||
|
|
||||||
|
/** 联系组ID */
|
||||||
|
@Column(name = "group_id", nullable = false)
|
||||||
|
private Integer groupId;
|
||||||
|
|
||||||
|
/** 用户ID(基础平台用户ID,决策 D4:身份只存 id) */
|
||||||
|
@Column(name = "user_id", nullable = false)
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
/** 用户姓名快照(仅展示用,可回写;账号不落库) */
|
||||||
|
@Column(name = "user_name", length = 64)
|
||||||
|
private String userName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍楼栋。房间数/床位数/入住数由房间与住宿生派生,不落库。
|
||||||
|
* 对应前端 DormBuilding。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_building")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class DormBuilding extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "building_name", nullable = false, length = 100)
|
||||||
|
private String buildingName;
|
||||||
|
|
||||||
|
/** 男 / 女 / 混合 */
|
||||||
|
@Column(name = "gender", length = 16)
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Column(name = "floor_count")
|
||||||
|
private Integer floorCount = 0;
|
||||||
|
|
||||||
|
/** 每层房间数(批量建房用) */
|
||||||
|
@Column(name = "rooms_per_floor")
|
||||||
|
private Integer roomsPerFloor = 0;
|
||||||
|
|
||||||
|
/** 每间床位数 */
|
||||||
|
@Column(name = "beds_per_room")
|
||||||
|
private Integer bedsPerRoom = 0;
|
||||||
|
|
||||||
|
/** 房间号前缀(如 "1" → roomId 以 1 开头) */
|
||||||
|
@Column(name = "room_prefix", length = 16)
|
||||||
|
private String roomPrefix;
|
||||||
|
|
||||||
|
/** 宿管负责人 */
|
||||||
|
@Column(name = "supervisor_name", length = 64)
|
||||||
|
private String supervisorName;
|
||||||
|
|
||||||
|
@Column(name = "supervisor_phone", length = 32)
|
||||||
|
private String supervisorPhone;
|
||||||
|
|
||||||
|
/** 多租户隔离 */
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知阅读记录(学生级)。家长=登录学生本人,打开通知即落一条(notice_id + student_id 唯一)。
|
||||||
|
* 用于「已读人数」按实际阅读的学生计数(区别于房间级 read_room_ids 的阅读情况分组)。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_notice_read",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_notice_student", columnNames = {"notice_id", "student_id"}))
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class DormNoticeRead extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "notice_id", nullable = false)
|
||||||
|
private Integer noticeId;
|
||||||
|
|
||||||
|
/** 阅读学生平台用户 id(决策 D4:只存 id) */
|
||||||
|
@Column(name = "student_id", nullable = false)
|
||||||
|
private Integer studentId;
|
||||||
|
|
||||||
|
@Column(name = "room_id")
|
||||||
|
private Integer roomId;
|
||||||
|
|
||||||
|
@Column(name = "read_at")
|
||||||
|
private LocalDateTime readAt;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍房间(通知的接收对象,房间树叶子)。业务主键 roomId = buildingId*10000 + floor*100 + 序号。
|
||||||
|
* 对应前端 DormRoom。usedBeds / monitorName 由住宿生派生,不落库。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_room")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class DormRoom extends BaseEntity {
|
||||||
|
|
||||||
|
/** 业务房间号(全局唯一,接收对象按此匹配) */
|
||||||
|
@Column(name = "room_id", nullable = false, unique = true)
|
||||||
|
private Integer roomId;
|
||||||
|
|
||||||
|
@Column(name = "room_no", nullable = false, length = 32)
|
||||||
|
private String roomNo;
|
||||||
|
|
||||||
|
@Column(name = "building_id", nullable = false)
|
||||||
|
private Integer buildingId;
|
||||||
|
|
||||||
|
@Column(name = "floor")
|
||||||
|
private Integer floor = 0;
|
||||||
|
|
||||||
|
/** 床位总数 */
|
||||||
|
@Column(name = "bed_count")
|
||||||
|
private Integer bedCount = 0;
|
||||||
|
|
||||||
|
@Column(name = "supervisor_name", length = 64)
|
||||||
|
private String supervisorName;
|
||||||
|
|
||||||
|
@Column(name = "supervisor_phone", length = 32)
|
||||||
|
private String supervisorPhone;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 住宿生(床位安排的最小单位,1 床 1 生)。roomId 为空/0 表示「待安排池」(对应前端 PendingStudent),
|
||||||
|
* 非空表示在住(对应前端 DormStudent)。studentId 为平台学生数字 id(决策 D4:只存 id,不存账号)。
|
||||||
|
* usedBeds 恒等于房内在住记录数(单一数据源,不反向凭空造学生)。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_student")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class DormStudent extends BaseEntity {
|
||||||
|
|
||||||
|
/** 平台学生数字 id(唯一业务键,家长登录态据此匹配) */
|
||||||
|
@Column(name = "student_id", nullable = false)
|
||||||
|
private Integer studentId;
|
||||||
|
|
||||||
|
@Column(name = "student_name", length = 64)
|
||||||
|
private String studentName;
|
||||||
|
|
||||||
|
/** 男 / 女 */
|
||||||
|
@Column(name = "gender", length = 8)
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
/** 文化班 / 武术班 */
|
||||||
|
@Column(name = "class_name", length = 64)
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
/** 所在房间业务号;0/空=待安排 */
|
||||||
|
@Column(name = "room_id")
|
||||||
|
private Integer roomId = 0;
|
||||||
|
|
||||||
|
/** 床位号,如 3号床 */
|
||||||
|
@Column(name = "bed_no", length = 16)
|
||||||
|
private String bedNo;
|
||||||
|
|
||||||
|
@Column(name = "guardian_name", length = 64)
|
||||||
|
private String guardianName;
|
||||||
|
|
||||||
|
@Column(name = "guardian_phone", length = 32)
|
||||||
|
private String guardianPhone;
|
||||||
|
|
||||||
|
/** 与学生关系 */
|
||||||
|
@Column(name = "relation", length = 16)
|
||||||
|
private String relation;
|
||||||
|
|
||||||
|
/** 是否宿舍长(每间房至多 1 人) */
|
||||||
|
@Column(name = "is_monitor")
|
||||||
|
private Boolean isMonitor = false;
|
||||||
|
|
||||||
|
/** 待安排来源说明 */
|
||||||
|
@Column(name = "source_desc", length = 128)
|
||||||
|
private String sourceDesc;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍通知主表。收件人为「宿舍房间」(receiveList = ALL 或房间 id 逗号串,可含楼栋前缀 xx0000)。
|
||||||
|
* 应收/已读人数按覆盖房间的在住住宿生数派生。对应前端 NoticeVo / NoticeDetailVo。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_notice")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class Notice extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "title", nullable = false, length = 200)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "content", columnDefinition = "LONGTEXT")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/** 通知类型(对应 sys_dict noticeType 的 Name) */
|
||||||
|
@Column(name = "notice_type", length = 64)
|
||||||
|
private String noticeType;
|
||||||
|
|
||||||
|
/** 紧急程度 normal/important/urgent */
|
||||||
|
@Column(name = "level", length = 16)
|
||||||
|
private String level = "normal";
|
||||||
|
|
||||||
|
/** 生效时间 */
|
||||||
|
@Column(name = "effective_time")
|
||||||
|
private LocalDateTime effectiveTime;
|
||||||
|
|
||||||
|
/** 回执截止时间 */
|
||||||
|
@Column(name = "deadline_time")
|
||||||
|
private LocalDateTime deadlineTime;
|
||||||
|
|
||||||
|
/** 回执方式 none/confirm/fill */
|
||||||
|
@Column(name = "receipt_mode", length = 16)
|
||||||
|
private String receiptMode = "none";
|
||||||
|
|
||||||
|
/** 单据类型 stay/visit/adjust/general(仅 receiptMode=fill) */
|
||||||
|
@Column(name = "form_type", length = 16)
|
||||||
|
private String formType;
|
||||||
|
|
||||||
|
/** 发布部门 id(sys_dict dept 的 CodeID) */
|
||||||
|
@Column(name = "dept_id", length = 64)
|
||||||
|
private String deptId;
|
||||||
|
|
||||||
|
@Column(name = "dept_name", length = 128)
|
||||||
|
private String deptName;
|
||||||
|
|
||||||
|
/** 发布人平台用户 id(决策 D4:只存 id,不存账号) */
|
||||||
|
@Column(name = "publisher_id")
|
||||||
|
private Integer publisherId;
|
||||||
|
|
||||||
|
@Column(name = "publisher_name", length = 64)
|
||||||
|
private String publisherName;
|
||||||
|
|
||||||
|
/** 是否需要宿管到场配合 */
|
||||||
|
@Column(name = "need_supervisor")
|
||||||
|
private Integer needSupervisor = 0;
|
||||||
|
|
||||||
|
/** 流程状态 draft/published/closed/finished */
|
||||||
|
@Column(name = "notice_status", nullable = false, length = 16)
|
||||||
|
private String noticeStatus = "published";
|
||||||
|
|
||||||
|
@Column(name = "topmost")
|
||||||
|
private Integer topmost = 0;
|
||||||
|
|
||||||
|
/** 是否撤回 0/1 */
|
||||||
|
@Column(name = "is_pulled")
|
||||||
|
private Integer isPulled = 0;
|
||||||
|
|
||||||
|
/** 定时发送时间 */
|
||||||
|
@Column(name = "timing_send_time")
|
||||||
|
private LocalDateTime timingSendTime;
|
||||||
|
|
||||||
|
/** 短信标记 0/1 */
|
||||||
|
@Column(name = "sms_flag")
|
||||||
|
private Integer smsFlag = 0;
|
||||||
|
|
||||||
|
/** 接收对象类型:room */
|
||||||
|
@Column(name = "receive_type", length = 16)
|
||||||
|
private String receiveType = "room";
|
||||||
|
|
||||||
|
/** 接收列表:"ALL" 或房间 id 逗号串 */
|
||||||
|
@Column(name = "receive_list", columnDefinition = "LONGTEXT")
|
||||||
|
private String receiveList;
|
||||||
|
|
||||||
|
/** 附件 id 逗号串 */
|
||||||
|
@Column(name = "file_ids", length = 2000)
|
||||||
|
private String fileIds;
|
||||||
|
|
||||||
|
/** 转发/复用源通知 id */
|
||||||
|
@Column(name = "source_id")
|
||||||
|
private Integer sourceId;
|
||||||
|
|
||||||
|
/** 已读房间 id 逗号串(房间级阅读日志;驱动 roomRead / 阅读情况分组 / 家长端 read 标记) */
|
||||||
|
@Column(name = "read_room_ids", columnDefinition = "LONGTEXT")
|
||||||
|
private String readRoomIds;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回执 / 填报记录(住宿生 × 通知),并兼作阅读日志(read / readAt,见方案 R2 折中)。
|
||||||
|
* 仅「需确认 / 需填报」类通知产生记录,草稿不产生。status:none/confirmed/filled/canceled。
|
||||||
|
* 对应前端 ReceiptBrief / ReceiptDetailVo / MyReceiptInfo。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "dorm_receipt")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class Receipt extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "notice_id", nullable = false)
|
||||||
|
private Integer noticeId;
|
||||||
|
|
||||||
|
/** 学生平台用户 id(家长态据此匹配) */
|
||||||
|
@Column(name = "student_id", nullable = false)
|
||||||
|
private Integer studentId;
|
||||||
|
|
||||||
|
@Column(name = "student_name", length = 64)
|
||||||
|
private String studentName;
|
||||||
|
|
||||||
|
@Column(name = "room_id")
|
||||||
|
private Integer roomId;
|
||||||
|
|
||||||
|
@Column(name = "room_no", length = 32)
|
||||||
|
private String roomNo;
|
||||||
|
|
||||||
|
@Column(name = "building_id")
|
||||||
|
private Integer buildingId;
|
||||||
|
|
||||||
|
@Column(name = "building_name", length = 100)
|
||||||
|
private String buildingName;
|
||||||
|
|
||||||
|
@Column(name = "bed_no", length = 16)
|
||||||
|
private String bedNo;
|
||||||
|
|
||||||
|
@Column(name = "class_name", length = 64)
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
@Column(name = "guardian_name", length = 64)
|
||||||
|
private String guardianName;
|
||||||
|
|
||||||
|
@Column(name = "guardian_phone", length = 32)
|
||||||
|
private String guardianPhone;
|
||||||
|
|
||||||
|
@Column(name = "relation", length = 16)
|
||||||
|
private String relation;
|
||||||
|
|
||||||
|
/** none/confirmed/filled/canceled */
|
||||||
|
@Column(name = "status", nullable = false, length = 16)
|
||||||
|
private String status = "none";
|
||||||
|
|
||||||
|
/** 是否已读 */
|
||||||
|
@Column(name = "read_flag")
|
||||||
|
private Boolean read = false;
|
||||||
|
|
||||||
|
@Column(name = "read_at")
|
||||||
|
private LocalDateTime readAt;
|
||||||
|
|
||||||
|
@Column(name = "submitted_at")
|
||||||
|
private LocalDateTime submittedAt;
|
||||||
|
|
||||||
|
/** 填报内容 JSON(key→值) */
|
||||||
|
@Column(name = "fill_data", columnDefinition = "TEXT")
|
||||||
|
private String fillData;
|
||||||
|
|
||||||
|
@Column(name = "remark", length = 1000)
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "sys_dict")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class SysDict extends BaseEntity {
|
||||||
|
|
||||||
|
/** 字典类型(如 "dept"=发文部门) */
|
||||||
|
@Column(name = "dict_type", length = 50)
|
||||||
|
private String dictType;
|
||||||
|
|
||||||
|
/** 字典 key(原 CodeID) */
|
||||||
|
@Column(name = "dict_key", length = 100)
|
||||||
|
private String dictKey;
|
||||||
|
|
||||||
|
/** 字典 value(显示名称,原 Name) */
|
||||||
|
@Column(name = "dict_value", length = 200)
|
||||||
|
private String dictValue;
|
||||||
|
|
||||||
|
/** 排序号 */
|
||||||
|
@Column(name = "sort_order")
|
||||||
|
private Integer sortOrder = 0;
|
||||||
|
|
||||||
|
/** 是否启用:0 禁用 / 1 启用(原 Enable) */
|
||||||
|
@Column(name = "enable_flag")
|
||||||
|
private Integer enableFlag = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.yunhai.dormnotice.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.dormnotice.aspect.OperationLogAspect} 对 {@code @OperationLog} 方法环绕落库。
|
||||||
|
* 索引交由 ddl-auto / 建表脚本,实体不重复声明 @Table(indexes) 以免 update 时重复建索引告警。</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;
|
||||||
|
|
||||||
|
/** 业务状态: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,47 @@
|
|||||||
|
package com.yunhai.dormnotice.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件文件记录。对应原 .NET Common_UpFiles 表。
|
||||||
|
* <p>
|
||||||
|
* 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在
|
||||||
|
* application.yml 的 file.upload-path 目录下。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "up_file")
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class UpFile extends BaseEntity {
|
||||||
|
|
||||||
|
/** 应用类型(如 "notice"),用于区分不同业务的附件 */
|
||||||
|
@Column(name = "topic_type", length = 50)
|
||||||
|
private String topicType;
|
||||||
|
|
||||||
|
/** 关联主题ID(如通知 serid) */
|
||||||
|
@Column(name = "topic_id")
|
||||||
|
private Integer topicId;
|
||||||
|
|
||||||
|
/** 文件真实名称(用户上传时的原始文件名) */
|
||||||
|
@Column(name = "file_true_name", length = 255)
|
||||||
|
private String fileTrueName;
|
||||||
|
|
||||||
|
/** 文件存储路径(相对路径,如 notice/20260823/uuid.pdf) */
|
||||||
|
@Column(name = "file_name", length = 500)
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
/** 文件大小(字节) */
|
||||||
|
@Column(name = "file_size")
|
||||||
|
private Integer fileSize;
|
||||||
|
|
||||||
|
/** 文件后缀名(如 pdf, docx, jpg) */
|
||||||
|
@Column(name = "file_type", length = 20)
|
||||||
|
private String fileType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.yunhai.dormnotice.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一分页查询入参。pageIndex 从 1 起。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BasePageQuery {
|
||||||
|
private int pageIndex = 1;
|
||||||
|
private int pageSize = 10;
|
||||||
|
/** 通用模糊搜索关键词 */
|
||||||
|
private String keyword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.yunhai.dormnotice.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一分页响应外壳。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BasePageResult<T> {
|
||||||
|
/** 总记录数 */
|
||||||
|
private int total;
|
||||||
|
/** 当前页数据 */
|
||||||
|
private List<T> content;
|
||||||
|
/** 当前页码(从 1 起) */
|
||||||
|
private int pageIndex;
|
||||||
|
/** 每页条数 */
|
||||||
|
private int pageSize;
|
||||||
|
/** 总页数 */
|
||||||
|
private int totalPages;
|
||||||
|
/** 是否还有下一页 */
|
||||||
|
private boolean hasNext;
|
||||||
|
|
||||||
|
public BasePageResult(List<T> content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) {
|
||||||
|
this.content = content;
|
||||||
|
this.total = total;
|
||||||
|
this.pageIndex = pageIndex;
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.totalPages = totalPages;
|
||||||
|
this.hasNext = hasNext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.yunhai.dormnotice.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。
|
||||||
|
* 对应 C# BestSchool.Entity.Model.SchTeaList。
|
||||||
|
*/
|
||||||
|
public class SchTeaList {
|
||||||
|
|
||||||
|
/** 学校 GUID(SchSerID) */
|
||||||
|
@JsonProperty("SchSerID")
|
||||||
|
private String schSerID;
|
||||||
|
|
||||||
|
@JsonProperty("TeaTel")
|
||||||
|
private String teaTel;
|
||||||
|
|
||||||
|
/** 教师姓名 */
|
||||||
|
@JsonProperty("TeaName")
|
||||||
|
private String teaName;
|
||||||
|
|
||||||
|
/** 教师用户ID */
|
||||||
|
@JsonProperty("UserTeaSerID")
|
||||||
|
private Integer userTeaSerID;
|
||||||
|
|
||||||
|
@JsonProperty("TeaIDCard")
|
||||||
|
private String teaIDCard;
|
||||||
|
|
||||||
|
/** 教师工号 */
|
||||||
|
@JsonProperty("TeaNo")
|
||||||
|
private String teaNo;
|
||||||
|
|
||||||
|
/** 教师所属组ID列表(兼容旧字段 GroupIds) */
|
||||||
|
@JsonProperty("GroupIds")
|
||||||
|
private List<Integer> groupIds;
|
||||||
|
|
||||||
|
/** 教师所属组ID(接口现行字段 GroupID,单个) */
|
||||||
|
@JsonProperty("GroupID")
|
||||||
|
private Integer groupId;
|
||||||
|
|
||||||
|
public Integer getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupId(Integer groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSchSerID() {
|
||||||
|
return schSerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSchSerID(String schSerID) {
|
||||||
|
this.schSerID = schSerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTeaTel() {
|
||||||
|
return teaTel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTeaTel(String teaTel) {
|
||||||
|
this.teaTel = teaTel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTeaName() {
|
||||||
|
return teaName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTeaName(String teaName) {
|
||||||
|
this.teaName = teaName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUserTeaSerID() {
|
||||||
|
return userTeaSerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserTeaSerID(Integer userTeaSerID) {
|
||||||
|
this.userTeaSerID = userTeaSerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTeaIDCard() {
|
||||||
|
return teaIDCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTeaIDCard(String teaIDCard) {
|
||||||
|
this.teaIDCard = teaIDCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTeaNo() {
|
||||||
|
return teaNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTeaNo(String teaNo) {
|
||||||
|
this.teaNo = teaNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getGroupIds() {
|
||||||
|
return groupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupIds(List<Integer> groupIds) {
|
||||||
|
this.groupIds = groupIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.yunhai.dormnotice.model.bsch;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bschclass 班级 DTO({@code /oapi/VSchClass/GetVClassList})。学生 ClassID 指向 classId。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class SchClassDto {
|
||||||
|
|
||||||
|
@JsonProperty("ClassID")
|
||||||
|
private Integer classId;
|
||||||
|
|
||||||
|
@JsonProperty("ClassName")
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
/** 年级ID */
|
||||||
|
@JsonProperty("Levell")
|
||||||
|
private Integer gradeId;
|
||||||
|
|
||||||
|
@JsonProperty("sx")
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.yunhai.dormnotice.model.bsch;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bschclass 学校信息 DTO({@code /oapi/SchInfo/GetUserSchFirst})。
|
||||||
|
* 用户中心 schId → bschclass 侧 SchSerId(后续按校查学生/班级用)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class SchInfoDto {
|
||||||
|
|
||||||
|
/** 学校 GUID(bschclass 侧 SchSerId,字符串) */
|
||||||
|
@JsonProperty("SchSerId")
|
||||||
|
private String schSerId;
|
||||||
|
|
||||||
|
/** 用户中心学校组 ID(与 schId 同域) */
|
||||||
|
@JsonProperty("UserSchSerId")
|
||||||
|
private Integer userSchSerId;
|
||||||
|
|
||||||
|
@JsonProperty("SchName")
|
||||||
|
private String schName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.dormnotice.model.bsch;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bschclass 学生 DTO({@code /oapi/OAStu/*},SchStudent 视图)。
|
||||||
|
* account 仅上游契约字段(兼容反序列化),本系统不落库、不作身份映射;
|
||||||
|
* 学生身份以 {@code stuId}(UserStuSerID,= 平台用户ID)+ stuName 承载(决策 D4)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class SchStudentDto {
|
||||||
|
|
||||||
|
/** 学员登录账号(上游契约字段,仅兼容保留,不落库) */
|
||||||
|
@JsonProperty("Account")
|
||||||
|
private String account;
|
||||||
|
|
||||||
|
/** 学生ID(= 平台用户ID,家长登录态即此值) */
|
||||||
|
@JsonProperty("UserStuSerID")
|
||||||
|
private Integer stuId;
|
||||||
|
|
||||||
|
/** 学生姓名 */
|
||||||
|
@JsonProperty("StuName")
|
||||||
|
private String stuName;
|
||||||
|
|
||||||
|
/** 所在班级ID(sch_class.class_id) */
|
||||||
|
@JsonProperty("ClassID")
|
||||||
|
private Integer classId;
|
||||||
|
|
||||||
|
/** 所在组ID */
|
||||||
|
@JsonProperty("GroupID")
|
||||||
|
private Integer groupId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.dormnotice.model.notice;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一键已读结果视图对象(替代原 Map<String,Object> 返回)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class InitializeRedVo {
|
||||||
|
|
||||||
|
@JsonProperty("count")
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
public InitializeRedVo() {}
|
||||||
|
|
||||||
|
public InitializeRedVo(Integer count) {
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yunhai.dormnotice.model.notice;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传结果视图对象(替代原 Map<String,Object> 返回)。
|
||||||
|
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UpFileResultVo {
|
||||||
|
|
||||||
|
@JsonProperty("fileid")
|
||||||
|
private Integer fileid;
|
||||||
|
|
||||||
|
@JsonProperty("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("fileurl")
|
||||||
|
private String fileurl;
|
||||||
|
|
||||||
|
@JsonProperty("host")
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
@JsonProperty("type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty("size")
|
||||||
|
private Integer size;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yunhai.dormnotice.model.notice;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件视图对象(对应原 .NET FileObj)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UpFileVo {
|
||||||
|
|
||||||
|
/** 文件记录ID */
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/** 文件真实名称 */
|
||||||
|
private String fileTrueName;
|
||||||
|
|
||||||
|
/** 文件存储路径(相对路径) */
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
/** 文件大小(字节) */
|
||||||
|
private Integer fileSize;
|
||||||
|
|
||||||
|
/** 文件后缀名 */
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
/** 关联主题ID */
|
||||||
|
private Integer topicId;
|
||||||
|
|
||||||
|
/** 应用类型 */
|
||||||
|
private String topicType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.yunhai.dormnotice.model.notice;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户可访问模块(左菜单)视图对象。
|
||||||
|
* <p>将基础平台返回的扁平 {@code List<BoaModule>} 转换为前端 layout(参考 UserBase)期望的树结构:
|
||||||
|
* {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。</p>
|
||||||
|
*/
|
||||||
|
public class UserModuleVo {
|
||||||
|
|
||||||
|
/** 模块主键 id(基础平台 module id) */
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/** 菜单高亮匹配用:等价于后端模块 id */
|
||||||
|
private Integer modId;
|
||||||
|
|
||||||
|
/** 模块名称 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 父级模块 id(0 表示根节点) */
|
||||||
|
private Integer parentId;
|
||||||
|
|
||||||
|
/** 前端路由路径(由 BoaModule.path 映射而来) */
|
||||||
|
private String page;
|
||||||
|
|
||||||
|
/** 层级(0=根分组) */
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
/** 菜单图标 */
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/** 排序号 */
|
||||||
|
private Integer orderNumber;
|
||||||
|
|
||||||
|
/** 是否可用 */
|
||||||
|
private Boolean usable;
|
||||||
|
|
||||||
|
/** 子菜单 */
|
||||||
|
private List<UserModuleVo> children = new ArrayList<>();
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getModId() {
|
||||||
|
return modId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModId(Integer modId) {
|
||||||
|
this.modId = modId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getParentId() {
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParentId(Integer parentId) {
|
||||||
|
this.parentId = parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPage() {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPage(String page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLevel(Integer level) {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOrderNumber() {
|
||||||
|
return orderNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOrderNumber(Integer orderNumber) {
|
||||||
|
this.orderNumber = orderNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getUsable() {
|
||||||
|
return usable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsable(Boolean usable) {
|
||||||
|
this.usable = usable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserModuleVo> getChildren() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChildren(List<UserModuleVo> children) {
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.ContactsGroup;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ContactsGroupRepository extends JpaRepository<ContactsGroup, Integer> {
|
||||||
|
Page<ContactsGroup> findAllByOrderByCreateTimeDesc(Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按创建人用户ID查询联系组列表。
|
||||||
|
*/
|
||||||
|
List<ContactsGroup> findByCreatorId(Integer creatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按创建人用户ID查询联系组列表(分页)。
|
||||||
|
*/
|
||||||
|
Page<ContactsGroup> findByCreatorId(Integer creatorId, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部联系组(按创建时间降序)。
|
||||||
|
*/
|
||||||
|
List<ContactsGroup> findAllByOrderByCreateTimeDesc();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.ContactsGroupUser;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ContactsGroupUserRepository extends JpaRepository<ContactsGroupUser, Integer> {
|
||||||
|
Page<ContactsGroupUser> findByGroupId(Integer groupId, Pageable pageable);
|
||||||
|
List<ContactsGroupUser> findByGroupId(Integer groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。
|
||||||
|
* 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行,
|
||||||
|
* 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。
|
||||||
|
*/
|
||||||
|
@Modifying(clearAutomatically = true)
|
||||||
|
@Query("delete from ContactsGroupUser g where g.groupId = :groupId")
|
||||||
|
void deleteByGroupIdBulk(@Param("groupId") Integer groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按多个组ID批量删除成员。
|
||||||
|
*/
|
||||||
|
@Modifying(clearAutomatically = true)
|
||||||
|
@Query("delete from ContactsGroupUser g where g.groupId in :groupIds")
|
||||||
|
void deleteByGroupIdInBulk(@Param("groupIds") List<Integer> groupIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按多个组ID查询所有成员(用于统计每组人数、组树构建)。
|
||||||
|
*/
|
||||||
|
List<ContactsGroupUser> findByGroupIdIn(List<Integer> groupIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按组ID和用户ID查询(判断是否已存在)。
|
||||||
|
*/
|
||||||
|
boolean existsByGroupIdAndUserId(Integer groupId, Integer userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormBuilding;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DormBuildingRepository extends JpaRepository<DormBuilding, Integer> {
|
||||||
|
List<DormBuilding> findAllByOrderByIdAsc();
|
||||||
|
|
||||||
|
List<DormBuilding> findBySchIdOrderByIdAsc(Integer schId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormNoticeRead;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DormNoticeReadRepository extends JpaRepository<DormNoticeRead, Integer> {
|
||||||
|
long countByNoticeId(Integer noticeId);
|
||||||
|
|
||||||
|
boolean existsByNoticeIdAndStudentId(Integer noticeId, Integer studentId);
|
||||||
|
|
||||||
|
List<DormNoticeRead> findByNoticeId(Integer noticeId);
|
||||||
|
|
||||||
|
List<DormNoticeRead> findByNoticeIdAndStudentIdIn(Integer noticeId, List<Integer> studentIds);
|
||||||
|
|
||||||
|
void deleteByNoticeId(Integer noticeId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormRoom;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface DormRoomRepository extends JpaRepository<DormRoom, Integer> {
|
||||||
|
Optional<DormRoom> findByRoomId(Integer roomId);
|
||||||
|
|
||||||
|
List<DormRoom> findByRoomIdIn(List<Integer> roomIds);
|
||||||
|
|
||||||
|
List<DormRoom> findByBuildingIdOrderByIdAsc(Integer buildingId);
|
||||||
|
|
||||||
|
List<DormRoom> findAllByOrderByIdAsc();
|
||||||
|
|
||||||
|
long countByBuildingId(Integer buildingId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormStudent;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface DormStudentRepository extends JpaRepository<DormStudent, Integer> {
|
||||||
|
/** 在住:roomId>0;待安排池:roomId=0 */
|
||||||
|
List<DormStudent> findByRoomIdOrderByIdAsc(Integer roomId);
|
||||||
|
|
||||||
|
List<DormStudent> findByRoomIdIn(List<Integer> roomIds);
|
||||||
|
|
||||||
|
Optional<DormStudent> findByStudentId(Integer studentId);
|
||||||
|
|
||||||
|
List<DormStudent> findByStudentIdIn(List<Integer> studentIds);
|
||||||
|
|
||||||
|
long countByRoomId(Integer roomId);
|
||||||
|
|
||||||
|
List<DormStudent> findByRoomIdAndIsMonitorTrue(Integer roomId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.Notice;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface NoticeRepository extends JpaRepository<Notice, Integer>, JpaSpecificationExecutor<Notice> {
|
||||||
|
List<Notice> findByNoticeStatusIn(List<String> statuses);
|
||||||
|
|
||||||
|
List<Notice> findByPublisherId(Integer publisherId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.Receipt;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ReceiptRepository extends JpaRepository<Receipt, Integer> {
|
||||||
|
List<Receipt> findByNoticeId(Integer noticeId);
|
||||||
|
|
||||||
|
List<Receipt> findByNoticeIdIn(List<Integer> noticeIds);
|
||||||
|
|
||||||
|
Optional<Receipt> findByNoticeIdAndStudentId(Integer noticeId, Integer studentId);
|
||||||
|
|
||||||
|
List<Receipt> findByNoticeIdAndRoomId(Integer noticeId, Integer roomId);
|
||||||
|
|
||||||
|
List<Receipt> findByNoticeIdAndStatus(Integer noticeId, String status);
|
||||||
|
|
||||||
|
List<Receipt> findByStatusIn(List<String> statuses);
|
||||||
|
|
||||||
|
List<Receipt> findByStudentId(Integer studentId);
|
||||||
|
|
||||||
|
long countByNoticeIdAndStatus(Integer noticeId, String status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.SysDict;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface SysDictRepository extends JpaRepository<SysDict, Integer> {
|
||||||
|
List<SysDict> findByDictTypeOrderBySortOrderAsc(String dictType);
|
||||||
|
List<SysDict> findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag);
|
||||||
|
|
||||||
|
/** 按类型+key 取单条字典(发文部门名称解析用) */
|
||||||
|
Optional<SysDict> findByDictTypeAndDictKey(String dictType, String dictKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.SysOperationLog;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志仓储。
|
||||||
|
*/
|
||||||
|
public interface SysOperationLogRepository extends JpaRepository<SysOperationLog, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.yunhai.dormnotice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.UpFile;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
/**
|
||||||
|
* 附件文件仓储。
|
||||||
|
*/
|
||||||
|
public interface UpFileRepository extends JpaRepository<UpFile, Integer> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主题类型和主题ID查询附件列表。
|
||||||
|
*/
|
||||||
|
List<UpFile> findByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主题类型和多个主题ID批量查询附件(消除 N+1)。
|
||||||
|
*/
|
||||||
|
List<UpFile> findByTopicTypeAndTopicIdIn(String topicType, List<Integer> topicIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主题类型和主题ID删除附件(通知删除时级联清理)。
|
||||||
|
*/
|
||||||
|
void deleteByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.model.SchTeaList;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchClassDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchInfoDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchStudentDto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级管理(bschclass)开放接口客户端门面({@code /oapi/**} 免签,独立 RestTemplate 直连)。
|
||||||
|
* <p>参照 ClassHomeWork 的 ClassService:教师名单 + 学生名单。学生身份以 {@code UserStuSerID}
|
||||||
|
* (= 平台用户 id)承载(决策 D4:只认 id 不认账号)。schId→SchSerId 经 {@link #getSchFirst} 桥接。</p>
|
||||||
|
*/
|
||||||
|
public interface IClassService {
|
||||||
|
|
||||||
|
/** 全部教师:GET {bschclass}/oapi/DbClass/GetSchteaList?Group={schId} */
|
||||||
|
List<SchTeaList> getSchTeaList(int schId);
|
||||||
|
|
||||||
|
/** 用户中心 schId → bschclass 学校信息(含 SchSerId):GET /oapi/SchInfo/GetUserSchFirst?UserSchID= */
|
||||||
|
SchInfoDto getSchFirst(int schId);
|
||||||
|
|
||||||
|
/** 全校学生(待安排池/圈人数据源):GET /oapi/OAStu/GetSchStulist?schserid={schSerId} */
|
||||||
|
List<SchStudentDto> getSchStuList(String schSerId);
|
||||||
|
|
||||||
|
/** 按学生 id 批量查:GET /oapi/OAStu/GetStuIdslist?stuids=1,2,3 */
|
||||||
|
List<SchStudentDto> getStuIdsList(List<Integer> stuIds);
|
||||||
|
|
||||||
|
/** 全校班级(classId→className 映射):GET /oapi/VSchClass/GetVClassList?SchSerID= */
|
||||||
|
List<SchClassDto> getSchClassList(String schSerId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系组业务接口。
|
||||||
|
* 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。
|
||||||
|
*/
|
||||||
|
public interface IContactsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加联系组。
|
||||||
|
*
|
||||||
|
* @param name 组名
|
||||||
|
* @param type 类型(1 自定义组, 2 明信片)
|
||||||
|
*/
|
||||||
|
void addGroup(String name, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组列表(含每组人数,分页)。
|
||||||
|
*
|
||||||
|
* @param page 页码(从1起)
|
||||||
|
* @param count 每页条数
|
||||||
|
* @param type 类型(1 自定义组, 2 明信片)
|
||||||
|
* @return Map: { list, total }
|
||||||
|
*/
|
||||||
|
Map<String, Object> getGroup(Integer page, Integer count, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑联系组。
|
||||||
|
*
|
||||||
|
* @param id 组ID
|
||||||
|
* @param name 组名
|
||||||
|
* @param type 类型
|
||||||
|
*/
|
||||||
|
void editGroup(Integer id, String name, Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除联系组(支持批量,同时删除组内成员)。
|
||||||
|
*
|
||||||
|
* @param ids 逗号分隔的组ID
|
||||||
|
*/
|
||||||
|
void delGroup(String ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置联系组教师(全量替换:先删后增)。
|
||||||
|
*
|
||||||
|
* @param gid 组ID
|
||||||
|
* @param ids 逗号分隔的用户ID
|
||||||
|
*/
|
||||||
|
void setContactsTea(Integer gid, String ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组教师列表(分页)。
|
||||||
|
*
|
||||||
|
* @param gid 组ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param count 每页条数
|
||||||
|
* @return Map: { list, total }
|
||||||
|
*/
|
||||||
|
Map<String, Object> getContactsTea(String gid, Integer page, Integer count);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取联系组树(组节点+用户节点,递归构建)。
|
||||||
|
*
|
||||||
|
* @param type 类型(1 自定义组, 2 明信片)
|
||||||
|
* @return 树形结构 List<Map>
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getGroupTree(Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门角色组树。
|
||||||
|
* C# 从外部API获取部门树+用户角色映射。Java版预留。
|
||||||
|
*
|
||||||
|
* @return 树形结构 List<Map>
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getDeptRoleGroupTree();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统字典业务接口(宿舍通知版)。对应前端 dict 组:List / ManageList / Add / Update / Delete / Sort / Sync。
|
||||||
|
* 字典分两类:{@code dept} 发布部门、{@code noticeType} 通知类型,均落 sys_dict 表(dict_type 区分)。
|
||||||
|
*/
|
||||||
|
public interface IDictService {
|
||||||
|
|
||||||
|
/** 下拉:type=dept → {k:CodeID,v:Name};type=noticeType → {k:Name,v:Name};仅启用项。 */
|
||||||
|
List<Map<String, Object>> list(String type);
|
||||||
|
|
||||||
|
/** 管理列表:{SerID,CodeID,Name,Enable}。 */
|
||||||
|
List<Map<String, Object>> manageList(String type);
|
||||||
|
|
||||||
|
/** 新增字典项(noticeType 重名校验)。 */
|
||||||
|
void add(String name, String type);
|
||||||
|
|
||||||
|
/** 更新字典项(名称、启用状态)。 */
|
||||||
|
void update(Integer serId, String name, Integer enable, String type);
|
||||||
|
|
||||||
|
/** 删除字典项。 */
|
||||||
|
void delete(Integer serId, String type);
|
||||||
|
|
||||||
|
/** 排序:ids 逗号串按序落 sortOrder。 */
|
||||||
|
void sort(String ids, String type);
|
||||||
|
|
||||||
|
/** 是否开通短信(占位,默认 false)。 */
|
||||||
|
Boolean isSmsEnabled();
|
||||||
|
|
||||||
|
/** 按 dept 字典 CodeID 解析部门名称;未命中返回 null。 */
|
||||||
|
String resolveDeptName(String deptId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍设置业务(楼栋 / 房间 / 床位安排),对应前端 dormApi(/api/dorm/*)。
|
||||||
|
* 读接口返回与 mock 对齐的 Map 结构(派生字段 roomCount/bedCount/usedBeds 实时算,不落库)。
|
||||||
|
* 决策 D4:学生只认 studentId(平台用户 id),不存账号。
|
||||||
|
*/
|
||||||
|
public interface IDormService {
|
||||||
|
|
||||||
|
List<Map<String, Object>> buildingList();
|
||||||
|
|
||||||
|
List<Map<String, Object>> roomList(Integer buildingId, String keyword);
|
||||||
|
|
||||||
|
void addBuilding(Map<String, Object> data);
|
||||||
|
|
||||||
|
void editBuilding(Map<String, Object> data);
|
||||||
|
|
||||||
|
void delBuilding(Integer buildingId);
|
||||||
|
|
||||||
|
void addRoom(Map<String, Object> data);
|
||||||
|
|
||||||
|
void editRoom(Map<String, Object> data);
|
||||||
|
|
||||||
|
void delRoom(Integer roomId);
|
||||||
|
|
||||||
|
List<Map<String, Object>> roomStudents(Integer roomId);
|
||||||
|
|
||||||
|
List<Map<String, Object>> pendingStudents(String keyword, String gender);
|
||||||
|
|
||||||
|
void assignStudent(Integer roomId, List<Integer> studentIds);
|
||||||
|
|
||||||
|
void removeStudent(Integer roomId, Integer studentId);
|
||||||
|
|
||||||
|
Map<String, Object> transferStudent(Integer studentId, Integer fromRoomId, Integer toRoomId);
|
||||||
|
|
||||||
|
void setMonitor(Integer roomId, Integer studentId);
|
||||||
|
|
||||||
|
/** 楼栋→楼层→房间树(ztree,供发布选房间;对应 base/GetDormTree)。 */
|
||||||
|
List<Map<String, Object>> dormTree();
|
||||||
|
|
||||||
|
/** 从 bschclass 学籍导入全校学生到「待安排池」(roomId=0),幂等;返回新增人数。 */
|
||||||
|
int importStudentsFromBschclass();
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.UpFile;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileResultVo;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传/下载/删除业务接口。
|
||||||
|
*/
|
||||||
|
public interface IFileService {
|
||||||
|
/**
|
||||||
|
* 上传单个文件。
|
||||||
|
*
|
||||||
|
* @param file 上传的文件
|
||||||
|
* @param appType 应用类型(如 "notice")
|
||||||
|
* @param topicId 关联主题ID(可为空,通知保存后再绑定)
|
||||||
|
* @return 文件视图对象
|
||||||
|
*/
|
||||||
|
UpFileVo upload(MultipartFile file, String appType, Integer topicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将已上传但未绑定主题的附件绑定到指定通知。
|
||||||
|
*
|
||||||
|
* @param fileIds 文件记录ID列表
|
||||||
|
* @param topicId 通知ID
|
||||||
|
*/
|
||||||
|
void bindToTopic(List<Integer> fileIds, Integer topicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某主题下的附件列表。
|
||||||
|
*
|
||||||
|
* @param appType 应用类型
|
||||||
|
* @param topicId 主题ID
|
||||||
|
* @return 附件列表
|
||||||
|
*/
|
||||||
|
List<UpFileVo> listByTopic(String appType, Integer topicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件。
|
||||||
|
*
|
||||||
|
* @param fileId 文件记录ID
|
||||||
|
* @param response HTTP 响应
|
||||||
|
*/
|
||||||
|
void download(Integer fileId, HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线预览文件(Content-Disposition: inline,按扩展名输出真实 Content-Type,
|
||||||
|
* 供前端 open-file-viewer 取 Blob / video 播放)。
|
||||||
|
*
|
||||||
|
* @param fileId 文件记录ID
|
||||||
|
* @param response HTTP 响应
|
||||||
|
*/
|
||||||
|
void preview(Integer fileId, HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件(同时删除物理文件和数据库记录)。
|
||||||
|
*
|
||||||
|
* @param fileId 文件记录ID
|
||||||
|
*/
|
||||||
|
void delete(Integer fileId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包下载多个附件为 ZIP。
|
||||||
|
*
|
||||||
|
* @param files 附件记录列表(调用方按 fileIds 查好)
|
||||||
|
* @param zipName 压缩包显示名(自动追加 .zip)
|
||||||
|
* @param response HTTP 响应
|
||||||
|
*/
|
||||||
|
void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按主题类型和主题ID打包下载附件为 ZIP(Controller 便捷方法,内部自行查附件列表)。
|
||||||
|
*
|
||||||
|
* @param topicType 主题类型(如 "notice")
|
||||||
|
* @param topicId 主题ID
|
||||||
|
* @param zipName 压缩包显示名
|
||||||
|
* @param response HTTP 响应
|
||||||
|
*/
|
||||||
|
void downloadZipByTopic(String topicType, Integer topicId, String zipName, HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按附件ID集合打包下载为 ZIP(通知 AllFileDown 用:通知存的是 fileIds 逗号串)。
|
||||||
|
*/
|
||||||
|
void downloadZipByIds(List<Integer> fileIds, String zipName, HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量上传图片(循环上传,返回每个文件的视图对象列表)。
|
||||||
|
*/
|
||||||
|
List<UpFileVo> uploadImages(MultipartFile[] files, String appType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。
|
||||||
|
*/
|
||||||
|
UpFileResultVo uploadToResult(MultipartFile file, String appType);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍通知业务(通知 / 回执 / 家长端),对应前端 noticeApi(/api/notice/*)。
|
||||||
|
* 读接口返回与 mock toVo/toDetail/toParentVo 对齐的 Map(详情保留旧字段名 serid/Contents/DeptID…)。
|
||||||
|
* 身份(决策 D4 + 本轮澄清):家长端无独立家长账号,家长=登录学生本人,student_id = UserContext.getUserId()。
|
||||||
|
*/
|
||||||
|
public interface INoticeService {
|
||||||
|
|
||||||
|
/** 家长端收件箱(分页 + unreadCount)。 */
|
||||||
|
Map<String, Object> list(Map<String, Object> query);
|
||||||
|
|
||||||
|
/** 我发布的(scope=user) / 全部通知(分页)。 */
|
||||||
|
Map<String, Object> sendList(Map<String, Object> query);
|
||||||
|
|
||||||
|
/** 查看详情(type=1 标读家长本房间)。 */
|
||||||
|
Map<String, Object> detail(Integer serid, Integer type);
|
||||||
|
|
||||||
|
/** 编辑回显(不标读)。 */
|
||||||
|
Map<String, Object> getDetail(Integer serid);
|
||||||
|
|
||||||
|
/** 上一篇/下一篇。 */
|
||||||
|
Map<String, Object> nextPrev(Integer serid, String type);
|
||||||
|
|
||||||
|
/** 阅读情况:按楼栋分组,叶子=房间(read/unread)。 */
|
||||||
|
List<Map<String, Object>> readDetail(Integer serid);
|
||||||
|
|
||||||
|
/** 统一回执单模板清单。 */
|
||||||
|
List<Map<String, Object>> formTemplates();
|
||||||
|
|
||||||
|
Integer create(Map<String, Object> body);
|
||||||
|
|
||||||
|
void edit(Map<String, Object> body);
|
||||||
|
|
||||||
|
void delete(List<Integer> ids);
|
||||||
|
|
||||||
|
void userPull(Integer serid);
|
||||||
|
|
||||||
|
void setTopmost(Integer serid, Integer topmost);
|
||||||
|
|
||||||
|
Map<String, Object> expedite(Integer serid);
|
||||||
|
|
||||||
|
void receiveRooms(Integer serid, String rooms);
|
||||||
|
|
||||||
|
/** 某通知回执名单(按房间分组)+ 汇总。status: all/confirmed/filled/canceled/none。 */
|
||||||
|
Map<String, Object> receipts(Integer serid, String status);
|
||||||
|
|
||||||
|
Map<String, Object> receiptDetail(Integer receiptId);
|
||||||
|
|
||||||
|
void review(Map<String, Object> body);
|
||||||
|
|
||||||
|
Map<String, Object> remind(Integer serid);
|
||||||
|
|
||||||
|
/** 导出回执名单 CSV(含 BOM)。 */
|
||||||
|
String exportReceipts(Integer serid);
|
||||||
|
|
||||||
|
Map<String, Object> statis(String startTime, String endTime);
|
||||||
|
|
||||||
|
/** 家长端:本生所在房间可见通知(分桶计数 + list)。 */
|
||||||
|
Map<String, Object> myNotices(Map<String, Object> query);
|
||||||
|
|
||||||
|
/** 家长端:某通知详情 + 我的回执 + 填报字段。 */
|
||||||
|
Map<String, Object> myNotice(Integer serid);
|
||||||
|
|
||||||
|
Map<String, Object> submitReceipt(Map<String, Object> body);
|
||||||
|
|
||||||
|
void cancelReceipt(Integer serid);
|
||||||
|
|
||||||
|
/** 家长端一键已读本房间可见的未读通知。 */
|
||||||
|
Map<String, Object> initializeRed();
|
||||||
|
|
||||||
|
/** 某通知全部附件打包下载为 ZIP(对应 GET /notice/AllFileDown?dataid=)。 */
|
||||||
|
void allFileDown(Integer serid, javax.servlet.http.HttpServletResponse response);
|
||||||
|
|
||||||
|
/** 管理员代标记某学生已读该通知(写入学生级阅读记录)。 */
|
||||||
|
void markRead(Integer serid, Integer studentId);
|
||||||
|
|
||||||
|
/** 向某学生的家长催办该通知(短信通道未接入时为记录/回执占位)。 */
|
||||||
|
Map<String, Object> remindStudent(Integer serid, Integer studentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.yunhai.dormnotice.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。
|
||||||
|
* <p>封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用,
|
||||||
|
* 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层,
|
||||||
|
* Controller 只保留"取参 → 调 Service → 返回"的薄壳。</p>
|
||||||
|
*/
|
||||||
|
public interface IUserCenterService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户学校组及用户(树形结构)。
|
||||||
|
* <p>组装规则(从原 BaseApiController.getSchoolAll 下沉):</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组;</li>
|
||||||
|
* <li>bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下,
|
||||||
|
* 无有效组(如所属组全被剔除)的教师兜底挂学校根节点;</li>
|
||||||
|
* <li>扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @param schId 学校ID(用户当前角色所属学校)
|
||||||
|
* @return 用户树形结构(组节点 type=1 / 教师节点 type=2)
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getSchoolAllTree(Integer schId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定组内用户(教师 + 管理员)。
|
||||||
|
* <p>对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义
|
||||||
|
* (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。
|
||||||
|
* 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。</p>
|
||||||
|
*
|
||||||
|
* @param gid 组ID
|
||||||
|
* @param role 角色(默认 2=教师;1=管理员等)
|
||||||
|
* @return 组内用户列表
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getGroupUser(Integer gid, Integer role);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户可访问的模块树(左侧菜单)。
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.model.SchTeaList;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchClassDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchInfoDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchStudentDto;
|
||||||
|
import com.yunhai.dormnotice.service.IClassService;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.client.RestClientException;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bschclass 开放接口实现(教师 + 学生名单)。参照 ClassHomeWork 同名服务。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClassService implements IClassService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ClassService.class);
|
||||||
|
|
||||||
|
/** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */
|
||||||
|
@Value("${yh.open.bschclass:}")
|
||||||
|
private String bschClassHost;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
public ClassService() {
|
||||||
|
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||||
|
factory.setConnectTimeout(5000);
|
||||||
|
factory.setReadTimeout(10000);
|
||||||
|
this.restTemplate = new RestTemplate(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchTeaList> getSchTeaList(int schId) {
|
||||||
|
return exchangeList(bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId,
|
||||||
|
new ParameterizedTypeReference<List<SchTeaList>>() {
|
||||||
|
}, "获取教师数据", "schId=" + schId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SchInfoDto getSchFirst(int schId) {
|
||||||
|
requireHost();
|
||||||
|
String url = bschClassHost + "/oapi/SchInfo/GetUserSchFirst?UserSchID=" + schId;
|
||||||
|
try {
|
||||||
|
ResponseEntity<SchInfoDto> resp = restTemplate.getForEntity(URI.create(url), SchInfoDto.class);
|
||||||
|
return resp.getBody();
|
||||||
|
} catch (RestClientException e) {
|
||||||
|
log.error("[getSchFirst] 请求 bschclass 失败, url={}", url, e);
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchStudentDto> getSchStuList(String schSerId) {
|
||||||
|
return exchangeList(bschClassHost + "/oapi/OAStu/GetSchStulist?schserid=" + schSerId,
|
||||||
|
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||||
|
}, "获取学校学生", "schSerId=" + schSerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchStudentDto> getStuIdsList(List<Integer> stuIds) {
|
||||||
|
String csv = joinIds(stuIds);
|
||||||
|
return exchangeList(bschClassHost + "/oapi/OAStu/GetStuIdslist?stuids=" + csv,
|
||||||
|
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||||
|
}, "获取指定学生", "stuIds=" + csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchClassDto> getSchClassList(String schSerId) {
|
||||||
|
return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId,
|
||||||
|
new ParameterizedTypeReference<List<SchClassDto>>() {
|
||||||
|
}, "获取班级列表", "schSerId=" + schSerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 通用调用 ============================
|
||||||
|
|
||||||
|
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
|
||||||
|
String action, String arg) {
|
||||||
|
requireHost();
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
ResponseEntity<List<T>> resp = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), type);
|
||||||
|
List<T> list = resp.getBody();
|
||||||
|
log.info("[bschclass] {} 完成, {} 返回 {} 条", action, arg, list == null ? 0 : list.size());
|
||||||
|
return list != null ? list : Collections.emptyList();
|
||||||
|
} catch (RestClientException e) {
|
||||||
|
log.error("[bschclass] {} 失败, {}, url={}", action, arg, url, e);
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, action + "失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireHost() {
|
||||||
|
if (!StringUtils.hasText(bschClassHost)) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinIds(List<Integer> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return ids.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.ContactsGroup;
|
||||||
|
import com.yunhai.dormnotice.entity.ContactsGroupUser;
|
||||||
|
import com.yunhai.dormnotice.repository.ContactsGroupRepository;
|
||||||
|
import com.yunhai.dormnotice.repository.ContactsGroupUserRepository;
|
||||||
|
import com.yunhai.dormnotice.service.IContactsService;
|
||||||
|
import com.yunhai.dormnotice.utils.UserContext;
|
||||||
|
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.BoaUser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系组服务实现。
|
||||||
|
* 对应 C# ContactsController + ManageContacts.cs。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ContactsService implements IContactsService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ContactsService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ContactsGroupRepository groupRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ContactsGroupUserRepository groupUserRepository;
|
||||||
|
|
||||||
|
// ============================ AddGroup ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void addGroup(String name, Integer type) {
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字");
|
||||||
|
}
|
||||||
|
if (type == null) {
|
||||||
|
type = 1;
|
||||||
|
}
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
|
||||||
|
// 查重:同创建人下同名同类型组不重复
|
||||||
|
List<ContactsGroup> existing = groupRepository.findByCreatorId(uid);
|
||||||
|
boolean duplicate = existing.stream().anyMatch(g ->
|
||||||
|
name.equals(g.getGroupName()));
|
||||||
|
if (duplicate) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!");
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactsGroup group = new ContactsGroup();
|
||||||
|
group.setGroupName(name);
|
||||||
|
group.setCreatorId(uid);
|
||||||
|
groupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetGroup ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getGroup(Integer page, Integer count, Integer type) {
|
||||||
|
if (type == null) {
|
||||||
|
type = 1;
|
||||||
|
}
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
|
||||||
|
// 查询当前用户创建的全部联系组
|
||||||
|
List<ContactsGroup> allGroups = groupRepository.findByCreatorId(uid);
|
||||||
|
|
||||||
|
// 统计每组人数
|
||||||
|
List<Integer> groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
|
||||||
|
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
|
||||||
|
: groupUserRepository.findByGroupIdIn(groupIds);
|
||||||
|
Map<Integer, Long> countByGroup = allMembers.stream()
|
||||||
|
.collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 构建列表数据
|
||||||
|
List<Map<String, Object>> listData = allGroups.stream().map(g -> {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("serid", g.getId());
|
||||||
|
item.put("GroupName", g.getGroupName());
|
||||||
|
item.put("count", countByGroup.getOrDefault(g.getId(), 0L).intValue());
|
||||||
|
return item;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
int total = listData.size();
|
||||||
|
if (count != null && count > 0 && page != null && page > 0) {
|
||||||
|
int fromIndex = (page - 1) * count;
|
||||||
|
int toIndex = Math.min(fromIndex + count, total);
|
||||||
|
if (fromIndex < total) {
|
||||||
|
listData = listData.subList(fromIndex, toIndex);
|
||||||
|
} else {
|
||||||
|
listData = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", listData);
|
||||||
|
result.put("total", total);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ EditGroup ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void editGroup(Integer id, String name, Integer type) {
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空");
|
||||||
|
}
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||||
|
}
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
|
||||||
|
// 查重(排除自身)
|
||||||
|
List<ContactsGroup> existing = groupRepository.findByCreatorId(uid);
|
||||||
|
boolean duplicate = existing.stream().anyMatch(g ->
|
||||||
|
!g.getId().equals(id) && name.equals(g.getGroupName()));
|
||||||
|
if (duplicate) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组");
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactsGroup group = groupRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在"));
|
||||||
|
group.setGroupName(name);
|
||||||
|
groupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ DelGroup ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delGroup(String ids) {
|
||||||
|
if (!StringUtils.hasText(ids)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败");
|
||||||
|
}
|
||||||
|
List<Integer> idList = Arrays.stream(ids.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.map(Integer::parseInt)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 先删成员再删组
|
||||||
|
groupUserRepository.deleteByGroupIdInBulk(idList);
|
||||||
|
groupRepository.findAllById(idList).forEach(groupRepository::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ SetContactsTea ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void setContactsTea(Integer gid, String ids) {
|
||||||
|
if (gid == null || gid == 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||||
|
}
|
||||||
|
// 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush
|
||||||
|
// 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit)
|
||||||
|
groupUserRepository.deleteByGroupIdBulk(gid);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(ids)) {
|
||||||
|
List<Integer> userIds = Arrays.stream(ids.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.map(Integer::parseInt)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (Integer userId : userIds) {
|
||||||
|
ContactsGroupUser member = new ContactsGroupUser();
|
||||||
|
member.setGroupId(gid);
|
||||||
|
member.setUserId(userId);
|
||||||
|
// 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法),
|
||||||
|
// 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用
|
||||||
|
try {
|
||||||
|
BoaUser user = YschBaseOpenApi.getUserInfo(userId);
|
||||||
|
if (user != null) {
|
||||||
|
member.setUserName(user.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e);
|
||||||
|
}
|
||||||
|
groupUserRepository.save(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetContactsTea ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getContactsTea(String gid, Integer page, Integer count) {
|
||||||
|
Integer groupId = 0;
|
||||||
|
try {
|
||||||
|
groupId = Integer.parseInt(gid);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
groupId = 0;
|
||||||
|
}
|
||||||
|
if (groupId == 0) {
|
||||||
|
Map<String, Object> empty = new LinkedHashMap<>();
|
||||||
|
empty.put("list", Collections.emptyList());
|
||||||
|
empty.put("total", 0);
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ContactsGroupUser> allMembers = groupUserRepository.findByGroupId(groupId);
|
||||||
|
int total = allMembers.size();
|
||||||
|
|
||||||
|
// 先分页再补全用户信息(避免为未分页数据调用用户中心)
|
||||||
|
List<ContactsGroupUser> pageMembers = allMembers;
|
||||||
|
if (count != null && count > 0 && page != null && page > 0) {
|
||||||
|
int fromIndex = (page - 1) * count;
|
||||||
|
int toIndex = Math.min(fromIndex + count, total);
|
||||||
|
pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> listData = pageMembers.stream().map(m -> {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("sid", m.getId());
|
||||||
|
item.put("id", m.getUserId());
|
||||||
|
fillUserInfo(item, m);
|
||||||
|
return item;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", listData);
|
||||||
|
result.put("total", total);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。
|
||||||
|
* 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。
|
||||||
|
* 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 解析成员展示信息:姓名取快照,缺失时从用户中心反查(可回写姓名快照);
|
||||||
|
* account 为前端展示字段,运行时给出(用户中心账号,兜底用 userId 字符串),不落库(决策 D4)。
|
||||||
|
*/
|
||||||
|
private void fillUserInfo(Map<String, Object> item, ContactsGroupUser m) {
|
||||||
|
String name = m.getUserName();
|
||||||
|
String account = null;
|
||||||
|
if (m.getUserId() != null) {
|
||||||
|
BoaUser user = null;
|
||||||
|
try {
|
||||||
|
user = YschBaseOpenApi.getUserInfo(m.getUserId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e);
|
||||||
|
}
|
||||||
|
if (user != null) {
|
||||||
|
account = user.getAccount();
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
name = user.getName();
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
m.setUserName(name);
|
||||||
|
try {
|
||||||
|
groupUserRepository.save(m);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[fillUserInfo] 回写姓名快照失败, userId={}", m.getUserId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.put("name", name != null ? name : "");
|
||||||
|
item.put("account", StringUtils.hasText(account) ? account : String.valueOf(m.getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetGroupTree ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getGroupTree(Integer type) {
|
||||||
|
if (type == null) {
|
||||||
|
type = 1;
|
||||||
|
}
|
||||||
|
Integer uid = currentUserId();
|
||||||
|
|
||||||
|
// 查询当前用户的联系组
|
||||||
|
List<ContactsGroup> groups = groupRepository.findByCreatorId(uid);
|
||||||
|
List<Integer> groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 查询全部成员
|
||||||
|
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
|
||||||
|
: groupUserRepository.findByGroupIdIn(groupIds);
|
||||||
|
|
||||||
|
// 构建树节点
|
||||||
|
List<Map<String, Object>> treeNodes = new ArrayList<>();
|
||||||
|
|
||||||
|
// 组节点 (type=1, pid=0)
|
||||||
|
for (ContactsGroup g : groups) {
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", g.getId());
|
||||||
|
node.put("name", g.getGroupName());
|
||||||
|
node.put("pid", 0);
|
||||||
|
node.put("no", 0);
|
||||||
|
node.put("type", 1);
|
||||||
|
node.put("isParent", true);
|
||||||
|
node.put("photo", "");
|
||||||
|
node.put("children", new ArrayList<Map<String, Object>>());
|
||||||
|
treeNodes.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全
|
||||||
|
for (ContactsGroupUser m : allMembers) {
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", m.getUserId());
|
||||||
|
node.put("name", "");
|
||||||
|
node.put("pid", m.getGroupId());
|
||||||
|
node.put("no", 0);
|
||||||
|
node.put("type", 2);
|
||||||
|
node.put("isParent", false);
|
||||||
|
node.put("photo", "");
|
||||||
|
fillUserInfo(node, m);
|
||||||
|
treeNodes.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林
|
||||||
|
return formatUserTree(treeNodes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ GetDeptRoleGroupTree ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||||
|
// C# 从外部API获取部门树+用户角色映射。
|
||||||
|
// Java版暂未接入用户中心OpenAPI,返回空列表。
|
||||||
|
log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 辅助方法 ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
|
||||||
|
List<Map<String, Object>> data;
|
||||||
|
if (pid == 0) {
|
||||||
|
// 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的)
|
||||||
|
Set<Integer> allIds = treeList.stream()
|
||||||
|
.map(n -> (Integer) n.get("id"))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
data = treeList.stream()
|
||||||
|
.filter(n -> {
|
||||||
|
int p = (Integer) n.get("pid");
|
||||||
|
return p == 0 || !allIds.contains(p);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
data = treeList.stream()
|
||||||
|
.filter(n -> (Integer) n.get("pid") == pid)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map<String, Object> item : data) {
|
||||||
|
int itemId = (Integer) item.get("id");
|
||||||
|
int itemType = (Integer) item.get("type");
|
||||||
|
|
||||||
|
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
|
||||||
|
|
||||||
|
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
|
||||||
|
if (itemType == 1 && children.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||||
|
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||||
|
if (itemType == 1) {
|
||||||
|
node.put("children", children);
|
||||||
|
}
|
||||||
|
result.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 no 排序
|
||||||
|
result.sort(Comparator.comparingInt(n -> {
|
||||||
|
Object no = n.get("no");
|
||||||
|
if (no instanceof Integer) return (Integer) no;
|
||||||
|
if (no instanceof String) {
|
||||||
|
try { return Integer.parseInt((String) no); } catch (NumberFormatException e) { return 9999; }
|
||||||
|
}
|
||||||
|
return 9999;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前登录人平台用户ID(决策 D4:联系组按创建人用户ID隔离,不存账号)。
|
||||||
|
*/
|
||||||
|
private Integer currentUserId() {
|
||||||
|
return UserContext.getUserId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.SysDict;
|
||||||
|
import com.yunhai.dormnotice.repository.SysDictRepository;
|
||||||
|
import com.yunhai.dormnotice.service.IDictService;
|
||||||
|
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 org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统字典服务(宿舍通知版)。type 维度:dept 发布部门 / noticeType 通知类型。
|
||||||
|
* dict_key 存 CodeID(D001/T001…),dict_value 存展示名(Name)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DictService implements IDictService {
|
||||||
|
|
||||||
|
private static final String DEPT = "dept";
|
||||||
|
private static final String NOTICE_TYPE = "noticeType";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysDictRepository sysDictRepository;
|
||||||
|
|
||||||
|
private String normalizeType(String type) {
|
||||||
|
if (!StringUtils.hasText(type)) {
|
||||||
|
return NOTICE_TYPE;
|
||||||
|
}
|
||||||
|
return DEPT.equalsIgnoreCase(type) ? DEPT : NOTICE_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String codePrefix(String type) {
|
||||||
|
return DEPT.equals(type) ? "D" : "T";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ List(下拉) ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> list(String type) {
|
||||||
|
String t = normalizeType(type);
|
||||||
|
List<SysDict> items = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(t, 1);
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (SysDict d : items) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("k", NOTICE_TYPE.equals(t) ? d.getDictValue() : d.getDictKey());
|
||||||
|
m.put("v", d.getDictValue());
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ ManageList(管理) ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> manageList(String type) {
|
||||||
|
String t = normalizeType(type);
|
||||||
|
return sysDictRepository.findByDictTypeOrderBySortOrderAsc(t).stream().map(d -> {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("SerID", d.getId());
|
||||||
|
m.put("CodeID", d.getDictKey());
|
||||||
|
m.put("Name", d.getDictValue());
|
||||||
|
m.put("Enable", d.getEnableFlag());
|
||||||
|
return m;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Add ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void add(String name, String type) {
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!");
|
||||||
|
}
|
||||||
|
String trimmed = name.trim();
|
||||||
|
String t = normalizeType(type);
|
||||||
|
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(t);
|
||||||
|
boolean dup = existing.stream().anyMatch(d -> trimmed.equals(d.getDictValue()));
|
||||||
|
if (dup) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, NOTICE_TYPE.equals(t) ? "该通知类型已存在" : "已存在该分类,请重新填写!");
|
||||||
|
}
|
||||||
|
int maxSort = existing.stream().mapToInt(d -> d.getSortOrder() == null ? 0 : d.getSortOrder()).max().orElse(0);
|
||||||
|
SysDict dict = new SysDict();
|
||||||
|
dict.setDictType(t);
|
||||||
|
dict.setDictValue(trimmed);
|
||||||
|
dict.setSortOrder(maxSort + 1);
|
||||||
|
dict.setEnableFlag(1);
|
||||||
|
dict.setDictKey(codePrefix(t) + "000"); // 占位,save 后按 id 生成 CodeID
|
||||||
|
SysDict saved = sysDictRepository.save(dict);
|
||||||
|
saved.setDictKey(codePrefix(t) + String.format("%03d", saved.getId()));
|
||||||
|
sysDictRepository.save(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Update ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void update(Integer serId, String name, Integer enable, String type) {
|
||||||
|
if (serId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||||
|
}
|
||||||
|
SysDict dict = sysDictRepository.findById(serId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
String t = normalizeType(type);
|
||||||
|
boolean dup = sysDictRepository.findByDictTypeOrderBySortOrderAsc(t).stream()
|
||||||
|
.anyMatch(d -> !d.getId().equals(serId) && name.trim().equals(d.getDictValue()));
|
||||||
|
if (dup) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||||
|
}
|
||||||
|
dict.setDictValue(name.trim());
|
||||||
|
}
|
||||||
|
if (enable != null) {
|
||||||
|
dict.setEnableFlag(enable);
|
||||||
|
}
|
||||||
|
sysDictRepository.save(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Delete ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer serId, String type) {
|
||||||
|
SysDict dict = sysDictRepository.findById(serId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||||
|
// 通知引用校验在 NoticeService 侧按 deptId/noticeType 命中判定;此处直接删字典项。
|
||||||
|
sysDictRepository.delete(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Sort ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void sort(String ids, String type) {
|
||||||
|
if (!StringUtils.hasText(ids)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||||
|
}
|
||||||
|
int[] idArr = Arrays.stream(ids.split(","))
|
||||||
|
.map(String::trim).filter(s -> !s.isEmpty()).mapToInt(Integer::parseInt).toArray();
|
||||||
|
int index = idArr.length; // 递减赋序,保持前端顺序
|
||||||
|
for (int id : idArr) {
|
||||||
|
final int order = index--;
|
||||||
|
sysDictRepository.findById(id).ifPresent(d -> {
|
||||||
|
d.setSortOrder(order);
|
||||||
|
sysDictRepository.save(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Sync ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isSmsEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 部门名解析 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveDeptName(String deptId) {
|
||||||
|
if (!StringUtils.hasText(deptId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sysDictRepository.findByDictTypeAndDictKey(DEPT, deptId.trim())
|
||||||
|
.map(SysDict::getDictValue).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,532 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.DormBuilding;
|
||||||
|
import com.yunhai.dormnotice.entity.DormRoom;
|
||||||
|
import com.yunhai.dormnotice.entity.DormStudent;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchClassDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchInfoDto;
|
||||||
|
import com.yunhai.dormnotice.model.bsch.SchStudentDto;
|
||||||
|
import com.yunhai.dormnotice.repository.DormBuildingRepository;
|
||||||
|
import com.yunhai.dormnotice.repository.DormRoomRepository;
|
||||||
|
import com.yunhai.dormnotice.repository.DormStudentRepository;
|
||||||
|
import com.yunhai.dormnotice.service.IClassService;
|
||||||
|
import com.yunhai.dormnotice.service.IDormService;
|
||||||
|
import com.yunhai.dormnotice.utils.UserContext;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宿舍设置业务实现。房间树 id 规则:roomId = buildingId*10000 + floor*100 + 序号。
|
||||||
|
* usedBeds 恒等于房内在住学生数(单一数据源);床位号随入住/退宿重排为 1..n。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DormService implements IDormService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DormService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DormBuildingRepository buildingRepository;
|
||||||
|
@Autowired
|
||||||
|
private DormRoomRepository roomRepository;
|
||||||
|
@Autowired
|
||||||
|
private DormStudentRepository studentRepository;
|
||||||
|
@Autowired
|
||||||
|
private IClassService classService;
|
||||||
|
|
||||||
|
// ============================ 读 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> buildingList() {
|
||||||
|
List<DormBuilding> buildings = buildingsOfSch();
|
||||||
|
List<DormRoom> rooms = roomRepository.findAllByOrderByIdAsc();
|
||||||
|
Map<Integer, List<DormRoom>> byBldg = rooms.stream().collect(Collectors.groupingBy(DormRoom::getBuildingId));
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (DormBuilding b : buildings) {
|
||||||
|
List<DormRoom> br = byBldg.getOrDefault(b.getId(), Collections.emptyList());
|
||||||
|
int bedCount = br.stream().mapToInt(r -> nz(r.getBedCount())).sum();
|
||||||
|
int used = br.stream().mapToInt(r -> usedBeds(r.getRoomId())).sum();
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("buildingId", b.getId());
|
||||||
|
m.put("buildingName", b.getBuildingName());
|
||||||
|
m.put("gender", b.getGender());
|
||||||
|
m.put("floorCount", nz(b.getFloorCount()));
|
||||||
|
m.put("supervisorName", b.getSupervisorName());
|
||||||
|
m.put("supervisorPhone", b.getSupervisorPhone());
|
||||||
|
m.put("roomCount", br.size());
|
||||||
|
m.put("bedCount", bedCount);
|
||||||
|
m.put("usedBeds", used);
|
||||||
|
m.put("roomPrefix", b.getRoomPrefix());
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> roomList(Integer buildingId, String keyword) {
|
||||||
|
List<DormRoom> rooms = buildingId != null && buildingId > 0
|
||||||
|
? roomRepository.findByBuildingIdOrderByIdAsc(buildingId)
|
||||||
|
: roomRepository.findAllByOrderByIdAsc();
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (DormRoom r : rooms) {
|
||||||
|
DormBuilding b = buildingRepository.findById(r.getBuildingId()).orElse(null);
|
||||||
|
String bn = b == null ? "" : b.getBuildingName();
|
||||||
|
if (StringUtils.hasText(keyword)) {
|
||||||
|
String hay = (bn + " " + r.getRoomNo());
|
||||||
|
if (!hay.contains(keyword)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("roomId", r.getRoomId());
|
||||||
|
m.put("roomNo", r.getRoomNo());
|
||||||
|
m.put("buildingId", r.getBuildingId());
|
||||||
|
m.put("buildingName", bn);
|
||||||
|
m.put("floor", nz(r.getFloor()));
|
||||||
|
m.put("bedCount", nz(r.getBedCount()));
|
||||||
|
m.put("usedBeds", usedBeds(r.getRoomId()));
|
||||||
|
m.put("supervisorName", r.getSupervisorName());
|
||||||
|
m.put("supervisorPhone", r.getSupervisorPhone());
|
||||||
|
m.put("monitorName", monitorName(r.getRoomId()));
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> roomStudents(Integer roomId) {
|
||||||
|
return studentRepository.findByRoomIdOrderByIdAsc(roomId).stream().map(this::toStudentVo).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> pendingStudents(String keyword, String gender) {
|
||||||
|
// 待安排池:dorm_student 中 roomId=0/null。池空时惰性从 bschclass 学籍导入一次,
|
||||||
|
// 让「安排入住」有数据源;导入失败(平台不可达等)不影响页面,返回空池。
|
||||||
|
List<DormStudent> raw = studentRepository.findByRoomIdOrderByIdAsc(0);
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
try {
|
||||||
|
int n = importStudentsFromBschclass();
|
||||||
|
if (n > 0) {
|
||||||
|
raw = studentRepository.findByRoomIdOrderByIdAsc(0);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[pendingStudents] 从 bschclass 导入学生失败,返回空池: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<DormStudent> pool = raw.stream()
|
||||||
|
.filter(s -> !StringUtils.hasText(gender) || gender.equals(s.getGender()))
|
||||||
|
.filter(s -> !StringUtils.hasText(keyword)
|
||||||
|
|| (s.getStudentName() + s.getClassName() + s.getGuardianName()).contains(keyword))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (DormStudent s : pool) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("studentId", s.getStudentId());
|
||||||
|
m.put("studentName", s.getStudentName());
|
||||||
|
m.put("gender", s.getGender());
|
||||||
|
m.put("className", s.getClassName());
|
||||||
|
m.put("guardianName", s.getGuardianName());
|
||||||
|
m.put("guardianPhone", s.getGuardianPhone());
|
||||||
|
m.put("sourceDesc", s.getSourceDesc());
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> dormTree() {
|
||||||
|
List<DormBuilding> buildings = buildingsOfSch();
|
||||||
|
List<DormRoom> rooms = roomRepository.findAllByOrderByIdAsc();
|
||||||
|
List<Map<String, Object>> tree = new ArrayList<>();
|
||||||
|
for (DormBuilding b : buildings) {
|
||||||
|
List<DormRoom> br = rooms.stream().filter(r -> Objects.equals(r.getBuildingId(), b.getId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<Integer, List<DormRoom>> byFloor = br.stream()
|
||||||
|
.collect(Collectors.groupingBy(r -> nz(r.getFloor()), TreeMap::new, Collectors.toList()));
|
||||||
|
List<Map<String, Object>> floorNodes = new ArrayList<>();
|
||||||
|
byFloor.forEach((fl, rlist) -> {
|
||||||
|
List<Map<String, Object>> roomNodes = rlist.stream().map(r -> {
|
||||||
|
Map<String, Object> rn = new LinkedHashMap<>();
|
||||||
|
rn.put("id", r.getRoomId());
|
||||||
|
rn.put("name", shortName(b) + " " + r.getRoomNo() + "(" + usedBeds(r.getRoomId()) + "/" + nz(r.getBedCount()) + " 床)");
|
||||||
|
rn.put("isParent", false);
|
||||||
|
rn.put("bedCount", nz(r.getBedCount()));
|
||||||
|
rn.put("usedBeds", usedBeds(r.getRoomId()));
|
||||||
|
rn.put("supervisorName", r.getSupervisorName());
|
||||||
|
return rn;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
Map<String, Object> fn = new LinkedHashMap<>();
|
||||||
|
fn.put("id", b.getId() * 10000 + fl * 100);
|
||||||
|
fn.put("name", shortName(b) + " " + fl + " 层");
|
||||||
|
fn.put("isParent", true);
|
||||||
|
fn.put("children", roomNodes);
|
||||||
|
floorNodes.add(fn);
|
||||||
|
});
|
||||||
|
Map<String, Object> bn = new LinkedHashMap<>();
|
||||||
|
bn.put("id", b.getId());
|
||||||
|
bn.put("name", b.getBuildingName());
|
||||||
|
bn.put("isParent", true);
|
||||||
|
bn.put("children", floorNodes);
|
||||||
|
tree.add(bn);
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 楼栋写 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void addBuilding(Map<String, Object> data) {
|
||||||
|
DormBuilding b = new DormBuilding();
|
||||||
|
b.setBuildingName(str(data.get("buildingName")));
|
||||||
|
b.setGender(str(data.get("gender")));
|
||||||
|
b.setFloorCount(intg(data.get("floorCount"), 0));
|
||||||
|
b.setRoomsPerFloor(intg(data.get("roomsPerFloor"), 0));
|
||||||
|
b.setBedsPerRoom(intg(data.get("bedsPerRoom"), intg(data.get("beds"), 0)));
|
||||||
|
b.setRoomPrefix(str(data.get("roomPrefix")));
|
||||||
|
b.setSupervisorName(str(data.get("supervisorName")));
|
||||||
|
b.setSupervisorPhone(str(data.get("supervisorPhone")));
|
||||||
|
b.setSchId(UserContext.getSchId());
|
||||||
|
if (!StringUtils.hasText(b.getBuildingName())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋名称不能为空");
|
||||||
|
}
|
||||||
|
buildingRepository.save(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void editBuilding(Map<String, Object> data) {
|
||||||
|
Integer id = intg(data.get("buildingId"), null);
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋ID不能为空");
|
||||||
|
}
|
||||||
|
DormBuilding b = buildingRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "楼栋不存在"));
|
||||||
|
if (data.containsKey("buildingName")) b.setBuildingName(str(data.get("buildingName")));
|
||||||
|
if (data.containsKey("gender")) b.setGender(str(data.get("gender")));
|
||||||
|
if (data.containsKey("floorCount")) b.setFloorCount(intg(data.get("floorCount"), b.getFloorCount()));
|
||||||
|
if (data.containsKey("supervisorName")) b.setSupervisorName(str(data.get("supervisorName")));
|
||||||
|
if (data.containsKey("supervisorPhone")) b.setSupervisorPhone(str(data.get("supervisorPhone")));
|
||||||
|
buildingRepository.save(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delBuilding(Integer buildingId) {
|
||||||
|
if (roomRepository.countByBuildingId(buildingId) > 0) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "请先清空该楼栋下的房间");
|
||||||
|
}
|
||||||
|
buildingRepository.deleteById(buildingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 房间写 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void addRoom(Map<String, Object> data) {
|
||||||
|
Integer buildingId = intg(data.get("buildingId"), null);
|
||||||
|
if (buildingId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "楼栋不能为空");
|
||||||
|
}
|
||||||
|
DormBuilding b = buildingRepository.findById(buildingId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "楼栋不存在"));
|
||||||
|
int floor = intg(data.get("floor"), 1);
|
||||||
|
int bedCount = intg(data.get("bedCount"), intg(data.get("beds"), nz(b.getBedsPerRoom())));
|
||||||
|
int count = Math.max(intg(data.get("roomCount"), 1), 1); // 新增间数:一次追加 count 间
|
||||||
|
Integer explicitRoomId = intg(data.get("roomId"), null);
|
||||||
|
// 该层已有房间数,向后追加序号避免重号
|
||||||
|
int seq = (int) roomRepository.findByBuildingIdOrderByIdAsc(buildingId).stream()
|
||||||
|
.filter(r -> nz(r.getFloor()) == floor).count();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int roomId;
|
||||||
|
if (explicitRoomId != null && count == 1) {
|
||||||
|
roomId = explicitRoomId;
|
||||||
|
if (roomRepository.findByRoomId(roomId).isPresent()) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "房间号已存在:" + roomId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
seq++;
|
||||||
|
roomId = buildingId * 10000 + floor * 100 + seq;
|
||||||
|
} while (roomRepository.findByRoomId(roomId).isPresent());
|
||||||
|
}
|
||||||
|
DormRoom r = new DormRoom();
|
||||||
|
r.setRoomId(roomId);
|
||||||
|
r.setRoomNo(count == 1 && StringUtils.hasText(str(data.get("roomNo")))
|
||||||
|
? str(data.get("roomNo"))
|
||||||
|
: floor + String.format("%02d", roomId % 100));
|
||||||
|
r.setBuildingId(buildingId);
|
||||||
|
r.setFloor(floor);
|
||||||
|
r.setBedCount(bedCount);
|
||||||
|
r.setSupervisorName(StringUtils.hasText(str(data.get("supervisorName"))) ? str(data.get("supervisorName")) : b.getSupervisorName());
|
||||||
|
r.setSupervisorPhone(b.getSupervisorPhone());
|
||||||
|
r.setSchId(UserContext.getSchId());
|
||||||
|
roomRepository.save(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void editRoom(Map<String, Object> data) {
|
||||||
|
Integer roomId = intg(data.get("roomId"), null);
|
||||||
|
if (roomId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "房间不能为空");
|
||||||
|
}
|
||||||
|
DormRoom r = roomRepository.findByRoomId(roomId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "房间不存在"));
|
||||||
|
if (data.containsKey("roomNo")) r.setRoomNo(str(data.get("roomNo")));
|
||||||
|
if (data.containsKey("supervisorName")) r.setSupervisorName(str(data.get("supervisorName")));
|
||||||
|
if (data.containsKey("supervisorPhone")) r.setSupervisorPhone(str(data.get("supervisorPhone")));
|
||||||
|
if (data.containsKey("bedCount")) {
|
||||||
|
int bc = intg(data.get("bedCount"), r.getBedCount());
|
||||||
|
long used = studentRepository.countByRoomId(roomId);
|
||||||
|
if (bc < used) {
|
||||||
|
// 床位收紧:超员学生退回待安排池
|
||||||
|
List<DormStudent> list = studentRepository.findByRoomIdOrderByIdAsc(roomId);
|
||||||
|
for (int i = bc; i < list.size(); i++) {
|
||||||
|
backToPool(list.get(i), "床位缩减待重新安排");
|
||||||
|
}
|
||||||
|
renumberBeds(roomId);
|
||||||
|
}
|
||||||
|
r.setBedCount(bc);
|
||||||
|
}
|
||||||
|
roomRepository.save(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delRoom(Integer roomId) {
|
||||||
|
if (studentRepository.countByRoomId(roomId) > 0) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "房间内仍有入住学生,无法删除");
|
||||||
|
}
|
||||||
|
roomRepository.findByRoomId(roomId).ifPresent(roomRepository::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 床位安排 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void assignStudent(Integer roomId, List<Integer> studentIds) {
|
||||||
|
DormRoom room = roomRepository.findByRoomId(roomId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "房间不存在"));
|
||||||
|
for (Integer sid : studentIds) {
|
||||||
|
DormStudent s = studentRepository.findByStudentId(sid)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "学生不存在:" + sid));
|
||||||
|
if (nz(s.getRoomId()) != 0) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, s.getStudentName() + " 已安排床位");
|
||||||
|
}
|
||||||
|
checkGender(room, s);
|
||||||
|
if (studentRepository.countByRoomId(roomId) >= nz(room.getBedCount())) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, roomShort(room) + " 床位已满");
|
||||||
|
}
|
||||||
|
s.setRoomId(roomId);
|
||||||
|
s.setSourceDesc(null);
|
||||||
|
studentRepository.save(s);
|
||||||
|
}
|
||||||
|
renumberBeds(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void removeStudent(Integer roomId, Integer studentId) {
|
||||||
|
studentRepository.findByStudentId(studentId).ifPresent(s -> {
|
||||||
|
if (Objects.equals(s.getRoomId(), roomId)) {
|
||||||
|
backToPool(s, "退宿待重新安排");
|
||||||
|
renumberBeds(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> transferStudent(Integer studentId, Integer fromRoomId, Integer toRoomId) {
|
||||||
|
DormStudent s = studentRepository.findByStudentId(studentId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "学生不存在"));
|
||||||
|
DormRoom to = roomRepository.findByRoomId(toRoomId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "目标房间不存在"));
|
||||||
|
checkGender(to, s);
|
||||||
|
if (studentRepository.countByRoomId(toRoomId) >= nz(to.getBedCount())) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, roomShort(to) + " 床位已满");
|
||||||
|
}
|
||||||
|
boolean wasMonitor = Boolean.TRUE.equals(s.getIsMonitor());
|
||||||
|
s.setRoomId(toRoomId);
|
||||||
|
s.setIsMonitor(false);
|
||||||
|
studentRepository.save(s);
|
||||||
|
renumberBeds(fromRoomId);
|
||||||
|
renumberBeds(toRoomId);
|
||||||
|
Map<String, Object> r = new LinkedHashMap<>();
|
||||||
|
r.put("wasMonitor", wasMonitor);
|
||||||
|
r.put("bedNo", studentRepository.findByStudentId(studentId).map(DormStudent::getBedNo).orElse(""));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void setMonitor(Integer roomId, Integer studentId) {
|
||||||
|
List<DormStudent> list = studentRepository.findByRoomIdOrderByIdAsc(roomId);
|
||||||
|
for (DormStudent s : list) {
|
||||||
|
s.setIsMonitor(studentId != null && studentId > 0 && s.getStudentId().equals(studentId));
|
||||||
|
studentRepository.save(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ bschclass 学籍导入 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int importStudentsFromBschclass() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "未获取到学校上下文,无法导入学生");
|
||||||
|
}
|
||||||
|
SchInfoDto info = classService.getSchFirst(schId);
|
||||||
|
if (info == null || !StringUtils.hasText(info.getSchSerId())) {
|
||||||
|
log.warn("[importStudents] schId={} 未取到 bschclass SchSerId", schId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String schSerId = info.getSchSerId();
|
||||||
|
List<SchStudentDto> stus = classService.getSchStuList(schSerId);
|
||||||
|
if (stus == null || stus.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Map<Integer, String> classNames = new HashMap<>();
|
||||||
|
try {
|
||||||
|
for (SchClassDto c : classService.getSchClassList(schSerId)) {
|
||||||
|
classNames.put(c.getClassId(), c.getClassName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[importStudents] 取班级名单失败(班级名留空): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
int added = 0;
|
||||||
|
for (SchStudentDto s : stus) {
|
||||||
|
if (s.getStuId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (studentRepository.findByStudentId(s.getStuId()).isPresent()) {
|
||||||
|
continue; // 已存在(在住或已在池)跳过,幂等
|
||||||
|
}
|
||||||
|
DormStudent d = new DormStudent();
|
||||||
|
d.setStudentId(s.getStuId());
|
||||||
|
d.setStudentName(s.getStuName());
|
||||||
|
d.setClassName(classNames.get(s.getClassId()));
|
||||||
|
d.setRoomId(0);
|
||||||
|
d.setIsMonitor(false);
|
||||||
|
d.setSourceDesc("学籍在册未分配床位");
|
||||||
|
d.setSchId(schId);
|
||||||
|
studentRepository.save(d);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
log.info("[importStudents] schId={} schSerId={} 学籍 {} 人,新导入待安排 {} 人", schId, schSerId, stus.size(), added);
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 辅助 ============================
|
||||||
|
|
||||||
|
private List<DormBuilding> buildingsOfSch() {
|
||||||
|
Integer schId = UserContext.getSchId();
|
||||||
|
return schId != null ? buildingRepository.findBySchIdOrderByIdAsc(schId) : buildingRepository.findAllByOrderByIdAsc();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backToPool(DormStudent s, String source) {
|
||||||
|
s.setRoomId(0);
|
||||||
|
s.setBedNo(null);
|
||||||
|
s.setIsMonitor(false);
|
||||||
|
s.setSourceDesc(source);
|
||||||
|
studentRepository.save(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 床位号重排为 1..n 连续;若无人任宿舍长,首位默认宿舍长。 */
|
||||||
|
private void renumberBeds(Integer roomId) {
|
||||||
|
List<DormStudent> list = studentRepository.findByRoomIdOrderByIdAsc(roomId);
|
||||||
|
boolean hasMonitor = list.stream().anyMatch(s -> Boolean.TRUE.equals(s.getIsMonitor()));
|
||||||
|
int i = 1;
|
||||||
|
for (DormStudent s : list) {
|
||||||
|
s.setBedNo(i++ + "号床");
|
||||||
|
studentRepository.save(s);
|
||||||
|
}
|
||||||
|
if (!list.isEmpty() && !hasMonitor) {
|
||||||
|
DormStudent first = list.get(0);
|
||||||
|
first.setIsMonitor(true);
|
||||||
|
studentRepository.save(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkGender(DormRoom room, DormStudent s) {
|
||||||
|
DormBuilding b = buildingRepository.findById(room.getBuildingId()).orElse(null);
|
||||||
|
if (b == null || s.getGender() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String g = b.getGender();
|
||||||
|
if (g != null && g.contains("女") && !"女".equals(s.getGender())) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "女生宿舍楼不能安排男生");
|
||||||
|
}
|
||||||
|
if (g != null && g.contains("男") && !"男".equals(s.getGender()) && !g.contains("混合")) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "男生宿舍楼不能安排女生");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String monitorName(Integer roomId) {
|
||||||
|
return studentRepository.findByRoomIdAndIsMonitorTrue(roomId).stream()
|
||||||
|
.map(DormStudent::getStudentName).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int usedBeds(Integer roomId) {
|
||||||
|
return (int) studentRepository.countByRoomId(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toStudentVo(DormStudent s) {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("studentId", s.getStudentId());
|
||||||
|
m.put("studentName", s.getStudentName());
|
||||||
|
m.put("gender", s.getGender());
|
||||||
|
m.put("bedNo", s.getBedNo());
|
||||||
|
m.put("className", s.getClassName());
|
||||||
|
m.put("guardianName", s.getGuardianName());
|
||||||
|
m.put("guardianPhone", s.getGuardianPhone());
|
||||||
|
m.put("relation", s.getRelation());
|
||||||
|
m.put("isMonitor", Boolean.TRUE.equals(s.getIsMonitor()));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String roomShort(DormRoom r) {
|
||||||
|
DormBuilding b = buildingRepository.findById(r.getBuildingId()).orElse(null);
|
||||||
|
return (b == null ? "" : shortName(b)) + " " + r.getRoomNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String shortName(DormBuilding b) {
|
||||||
|
if (StringUtils.hasText(b.getRoomPrefix())) {
|
||||||
|
return b.getRoomPrefix() + "号楼";
|
||||||
|
}
|
||||||
|
return b.getBuildingName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nz(Integer v) {
|
||||||
|
return v == null ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String str(Object o) {
|
||||||
|
return o == null ? null : String.valueOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer intg(Object o, Integer dft) {
|
||||||
|
if (o == null) {
|
||||||
|
return dft;
|
||||||
|
}
|
||||||
|
if (o instanceof Number) {
|
||||||
|
return ((Number) o).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(String.valueOf(o).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return dft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.entity.UpFile;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.dormnotice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.dormnotice.repository.UpFileRepository;
|
||||||
|
import com.yunhai.dormnotice.service.IFileService;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传/下载/删除业务实现。
|
||||||
|
* <p>
|
||||||
|
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||||
|
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class FileService implements IFileService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FileService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UpFileRepository upFileRepository;
|
||||||
|
|
||||||
|
@Value("${file.upload-path}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
|
private static final String DEFAULT_APP_TYPE = "dormnotice";
|
||||||
|
|
||||||
|
// ============================ 上传 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "上传文件不得为空");
|
||||||
|
}
|
||||||
|
String originalName = file.getOriginalFilename();
|
||||||
|
if (!StringUtils.hasText(originalName)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "文件名为空");
|
||||||
|
}
|
||||||
|
// 提取后缀
|
||||||
|
String ext = "";
|
||||||
|
int dotIdx = originalName.lastIndexOf('.');
|
||||||
|
if (dotIdx >= 0) {
|
||||||
|
ext = originalName.substring(dotIdx + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||||
|
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
|
||||||
|
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
|
||||||
|
|
||||||
|
// 物理写入
|
||||||
|
Path fullPath = Paths.get(uploadPath, relativePath);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(fullPath.getParent());
|
||||||
|
file.transferTo(fullPath.toFile());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[upload] 文件保存失败: {}", fullPath, e);
|
||||||
|
throw new BizException(ErrorCode.ERROR, "文件保存失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 入库
|
||||||
|
UpFile entity = new UpFile();
|
||||||
|
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType);
|
||||||
|
entity.setTopicId(topicId);
|
||||||
|
entity.setFileTrueName(originalName);
|
||||||
|
entity.setFileName(relativePath);
|
||||||
|
entity.setFileSize((int) file.getSize());
|
||||||
|
entity.setFileType(ext);
|
||||||
|
upFileRepository.save(entity);
|
||||||
|
|
||||||
|
return toVo(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 绑定主题 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void bindToTopic(List<Integer> fileIds, Integer topicId) {
|
||||||
|
if (fileIds == null || fileIds.isEmpty() || topicId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Integer fileId : fileIds) {
|
||||||
|
upFileRepository.findById(fileId).ifPresent(f -> {
|
||||||
|
f.setTopicId(topicId);
|
||||||
|
upFileRepository.save(f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 查询附件列表 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UpFileVo> listByTopic(String appType, Integer topicId) {
|
||||||
|
String type = appType == null ? DEFAULT_APP_TYPE : appType;
|
||||||
|
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
|
||||||
|
.map(this::toVo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 下载 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download(Integer fileId, HttpServletResponse response) {
|
||||||
|
writeFile(fileId, response, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preview(Integer fileId, HttpServletResponse response) {
|
||||||
|
writeFile(fileId, response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type
|
||||||
|
* (浏览器/前端组件可直接渲染),否则 attachment 强制下载。
|
||||||
|
*/
|
||||||
|
private void writeFile(Integer fileId, HttpServletResponse response, boolean inline) {
|
||||||
|
UpFile file = upFileRepository.findById(fileId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||||
|
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||||
|
if (!Files.exists(fullPath)) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "物理文件不存在: " + file.getFileName());
|
||||||
|
}
|
||||||
|
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
|
||||||
|
OutputStream os = response.getOutputStream()) {
|
||||||
|
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
if (inline) {
|
||||||
|
response.setContentType(resolveContentType(file.getFileName()));
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename=\"" + encodedName + "\"");
|
||||||
|
} else {
|
||||||
|
response.setContentType("application/octet-stream");
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\"");
|
||||||
|
}
|
||||||
|
response.setContentLengthLong(file.getFileSize());
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int len;
|
||||||
|
while ((len = fis.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[writeFile] 文件输出失败: {}", file.getFileName(), e);
|
||||||
|
throw new BizException(ErrorCode.ERROR, "文件下载失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按扩展名推断 Content-Type(JDK 自带表 + 常见办公/图片格式兜底),未知类型回退流格式。
|
||||||
|
*/
|
||||||
|
private String resolveContentType(String fileName) {
|
||||||
|
String name = String.valueOf(fileName).toLowerCase();
|
||||||
|
Map<String, String> table = new HashMap<>();
|
||||||
|
table.put("pdf", "application/pdf");
|
||||||
|
table.put("png", "image/png");
|
||||||
|
table.put("jpg", "image/jpeg");
|
||||||
|
table.put("jpeg", "image/jpeg");
|
||||||
|
table.put("gif", "image/gif");
|
||||||
|
table.put("bmp", "image/bmp");
|
||||||
|
table.put("webp", "image/webp");
|
||||||
|
table.put("svg", "image/svg+xml");
|
||||||
|
table.put("txt", "text/plain;charset=UTF-8");
|
||||||
|
table.put("mp4", "video/mp4");
|
||||||
|
table.put("webm", "video/webm");
|
||||||
|
table.put("ogg", "video/ogg");
|
||||||
|
table.put("doc", "application/msword");
|
||||||
|
table.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
table.put("xls", "application/vnd.ms-excel");
|
||||||
|
table.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
table.put("ppt", "application/vnd.ms-powerpoint");
|
||||||
|
table.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||||
|
String type = table.get(name.substring(name.lastIndexOf('.') + 1));
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
String guessed = URLConnection.guessContentTypeFromName(name);
|
||||||
|
return guessed != null ? guessed : "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 删除 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(Integer fileId) {
|
||||||
|
UpFile file = upFileRepository.findById(fileId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||||
|
// 删物理文件
|
||||||
|
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(fullPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
|
||||||
|
}
|
||||||
|
// 删数据库记录
|
||||||
|
upFileRepository.delete(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 打包下载 ZIP ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response) {
|
||||||
|
if (files == null || files.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "无附件可下载");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
response.setContentType("application/zip");
|
||||||
|
String name = URLEncoder.encode(zipName == null ? "附件" : zipName, "UTF-8")
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip\"");
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
|
||||||
|
Set<String> used = new HashSet<>();
|
||||||
|
for (UpFile f : files) {
|
||||||
|
Path p = Paths.get(uploadPath, f.getFileName());
|
||||||
|
if (!Files.exists(p)) {
|
||||||
|
log.warn("[downloadZip] 物理文件缺失,跳过: {}", f.getFileName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String entryName = sanitizeEntryName(f.getFileTrueName());
|
||||||
|
if (used.contains(entryName)) {
|
||||||
|
entryName = f.getId() + "_" + entryName;
|
||||||
|
}
|
||||||
|
used.add(entryName);
|
||||||
|
zos.putNextEntry(new ZipEntry(entryName));
|
||||||
|
Files.copy(p, zos);
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[downloadZip] 打包下载失败", e);
|
||||||
|
throw new BizException(ErrorCode.ERROR, "打包下载失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 净化 ZIP 条目名:去除路径分隔符等非法字符,防止 Zip Slip。
|
||||||
|
*/
|
||||||
|
private String sanitizeEntryName(String name) {
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
return "file";
|
||||||
|
}
|
||||||
|
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 按主题打包下载 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadZipByTopic(String topicType, Integer topicId, String zipName,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
String type = topicType == null ? DEFAULT_APP_TYPE : topicType;
|
||||||
|
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(type, topicId);
|
||||||
|
downloadZip(files, zipName, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadZipByIds(List<Integer> fileIds, String zipName, HttpServletResponse response) {
|
||||||
|
if (fileIds == null || fileIds.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "无附件可下载");
|
||||||
|
}
|
||||||
|
List<UpFile> files = upFileRepository.findAllById(fileIds);
|
||||||
|
downloadZip(files, zipName, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 批量上传图片 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public List<UpFileVo> uploadImages(MultipartFile[] files, String appType) {
|
||||||
|
List<UpFileVo> results = new ArrayList<>();
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
String type = appType != null ? appType : DEFAULT_APP_TYPE;
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
results.add(upload(file, type, null));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 上传(返回前端兼容 VO) ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public UpFileResultVo uploadToResult(MultipartFile file, String appType) {
|
||||||
|
UpFileVo vo = upload(file, appType, null);
|
||||||
|
UpFileResultVo result = new UpFileResultVo();
|
||||||
|
result.setFileid(vo.getId());
|
||||||
|
result.setName(vo.getFileTrueName());
|
||||||
|
result.setFileurl(vo.getFileName());
|
||||||
|
result.setHost("");
|
||||||
|
result.setType(vo.getFileType());
|
||||||
|
result.setSize(vo.getFileSize());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 转换 ============================
|
||||||
|
|
||||||
|
private UpFileVo toVo(UpFile entity) {
|
||||||
|
UpFileVo vo = new UpFileVo();
|
||||||
|
BeanUtils.copyProperties(entity, vo);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,268 @@
|
|||||||
|
package com.yunhai.dormnotice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.model.SchTeaList;
|
||||||
|
import com.yunhai.dormnotice.service.IClassService;
|
||||||
|
import com.yunhai.dormnotice.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.BoaGroup;
|
||||||
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户中心门面服务实现。
|
||||||
|
* <p>原 BaseApiController.getSchoolAll 的组树组装、教师挂组、递归建树、排序逻辑整体下沉至此;
|
||||||
|
* GetGroupUser 空桩改为显式报错(未接入用户中心对应开放接口时提示,而非静默返回空列表)。
|
||||||
|
* 跨仓调用统一收敛到 {@link YschBaseOpenApi} 门面,Controller 不再直接静态调用。</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UserCenterService implements IUserCenterService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(UserCenterService.class);
|
||||||
|
|
||||||
|
/** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),组树中剔除 */
|
||||||
|
private static final int GROUP_TYPE_STUDENT = 8;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IClassService classService;
|
||||||
|
|
||||||
|
// ============================ 学校组织树 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getSchoolAllTree(Integer schId) {
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. userbase 组树(整棵学校子树),剔除学生组
|
||||||
|
List<Map<String, Object>> flatNodes = new ArrayList<>();
|
||||||
|
BoaGroup root = YschBaseOpenApi.getGroupById(schId, true);
|
||||||
|
if (root == null) {
|
||||||
|
log.warn("[getSchoolAll] 用户中心未找到学校组, schId={}", schId);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
flattenGroup(root, 0, flatNodes);
|
||||||
|
|
||||||
|
// 2. bschclass 全部教师,按 GroupIds 挂到对应组下;无有效组的兜底挂学校根节点
|
||||||
|
Set<Integer> treeGroupIds = new HashSet<>();
|
||||||
|
for (Map<String, Object> node : flatNodes) {
|
||||||
|
treeGroupIds.add((Integer) node.get("id"));
|
||||||
|
}
|
||||||
|
Map<Integer, SchTeaList> teacherMap = new LinkedHashMap<>();
|
||||||
|
Map<Integer, Set<Integer>> teacherPids = new LinkedHashMap<>();
|
||||||
|
List<SchTeaList> teachers = classService.getSchTeaList(schId);
|
||||||
|
for (SchTeaList tea : teachers) {
|
||||||
|
if (tea.getUserTeaSerID() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 同一教师多行(多组/接口重复数据)合并,按 (教师, 组) 去重
|
||||||
|
teacherMap.putIfAbsent(tea.getUserTeaSerID(), tea);
|
||||||
|
// 接口现行字段为单个 GroupID,兼容旧字段 GroupIds
|
||||||
|
List<Integer> teaGroupIds = tea.getGroupIds();
|
||||||
|
if ((teaGroupIds == null || teaGroupIds.isEmpty()) && tea.getGroupId() != null) {
|
||||||
|
teaGroupIds = Collections.singletonList(tea.getGroupId());
|
||||||
|
}
|
||||||
|
if (teaGroupIds != null) {
|
||||||
|
for (Integer gid : teaGroupIds) {
|
||||||
|
if (gid != null && treeGroupIds.contains(gid)) {
|
||||||
|
teacherPids.computeIfAbsent(tea.getUserTeaSerID(), k -> new HashSet<>()).add(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, SchTeaList> entry : teacherMap.entrySet()) {
|
||||||
|
int teaId = entry.getKey();
|
||||||
|
SchTeaList tea = entry.getValue();
|
||||||
|
Set<Integer> pids = teacherPids.get(teaId);
|
||||||
|
if (pids == null || pids.isEmpty()) {
|
||||||
|
// 教师所属组均被剔除(如学生组)或无组信息,挂学校根节点
|
||||||
|
pids = new HashSet<>(Collections.singletonList(treeGroupIds.contains(schId) ? schId : 0));
|
||||||
|
}
|
||||||
|
for (Integer pid : pids) {
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", teaId);
|
||||||
|
node.put("name", tea.getTeaName() != null ? tea.getTeaName() : "");
|
||||||
|
node.put("pid", pid);
|
||||||
|
node.put("no", tea.getTeaNo() != null ? tea.getTeaNo() : 0);
|
||||||
|
node.put("type", 2);
|
||||||
|
node.put("isParent", false);
|
||||||
|
node.put("photo", "");
|
||||||
|
flatNodes.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 扁平节点递归组装为树
|
||||||
|
return formatUserTree(flatNodes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 BoaGroup 子树扁平化为组节点列表(type=1);学生组(type=8)整棵剔除。
|
||||||
|
* isParent=true 标识组节点,前端 ztree 据此渲染为文件夹。
|
||||||
|
*/
|
||||||
|
private void flattenGroup(BoaGroup group, int pid, List<Map<String, Object>> nodes) {
|
||||||
|
if (group == null || group.getId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (group.getType() != null && group.getType() == GROUP_TYPE_STUDENT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>();
|
||||||
|
node.put("id", group.getId());
|
||||||
|
node.put("name", group.getName() != null ? group.getName() : "");
|
||||||
|
node.put("pid", pid);
|
||||||
|
node.put("no", group.getSort() != null ? group.getSort() : 0);
|
||||||
|
node.put("type", 1);
|
||||||
|
node.put("isParent", true);
|
||||||
|
node.put("photo", "");
|
||||||
|
nodes.add(node);
|
||||||
|
|
||||||
|
if (group.getChildren() != null) {
|
||||||
|
for (BoaGroup child : group.getChildren()) {
|
||||||
|
flattenGroup(child, group.getId(), nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
|
||||||
|
* 根层取 pid=0 或父节点不在列表中的节点;type=1 且无子节点的空组剔除。
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
|
||||||
|
List<Map<String, Object>> data;
|
||||||
|
if (pid == 0) {
|
||||||
|
Set<Integer> allIds = new HashSet<>();
|
||||||
|
for (Map<String, Object> n : treeList) {
|
||||||
|
allIds.add((Integer) n.get("id"));
|
||||||
|
}
|
||||||
|
data = new ArrayList<>();
|
||||||
|
for (Map<String, Object> n : treeList) {
|
||||||
|
int p = (Integer) n.get("pid");
|
||||||
|
if (p == 0 || !allIds.contains(p)) {
|
||||||
|
data.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = new ArrayList<>();
|
||||||
|
for (Map<String, Object> n : treeList) {
|
||||||
|
if ((Integer) n.get("pid") == pid) {
|
||||||
|
data.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (Map<String, Object> item : data) {
|
||||||
|
int itemId = (Integer) item.get("id");
|
||||||
|
int itemType = (Integer) item.get("type");
|
||||||
|
|
||||||
|
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
|
||||||
|
|
||||||
|
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
|
||||||
|
if (itemType == 1 && children.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||||
|
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||||
|
if (itemType == 1) {
|
||||||
|
node.put("children", children);
|
||||||
|
}
|
||||||
|
result.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 no 排序(数字优先,无法解析的排最后)
|
||||||
|
result.sort(Comparator.comparingInt(n -> {
|
||||||
|
Object no = n.get("no");
|
||||||
|
if (no instanceof Integer) {
|
||||||
|
return (Integer) no;
|
||||||
|
}
|
||||||
|
if (no instanceof String) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt((String) no);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 9999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 9999;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 组内用户 ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getGroupUser(Integer gid, Integer role) {
|
||||||
|
if (gid == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "组ID不得为空");
|
||||||
|
}
|
||||||
|
log.info("[getGroupUser] gid={}, role={}, 用户中心组内用户开放接口尚未接入", gid, role);
|
||||||
|
// 显式报错而非静默返回空列表:原空桩会让前端误以为组内无用户
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "组内用户查询接口暂未接入用户中心,请联系管理员配置后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 用户模块树 ============================
|
||||||
|
|
||||||
|
@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()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 扁平 BoaModule → 树结构(modId/page/children),对齐前端 layout 期望
|
||||||
|
return buildModuleTree(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将扁平模块列表组装为树(对应原 NoticeController.buildModuleTree)。
|
||||||
|
* 顶层取 parentId=0 或父节点不在列表中的节点,子节点按 orderNumber 排序。
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
|
||||||
|
Map<Integer, BoaModule> moduleMap = modules.stream()
|
||||||
|
.collect(Collectors.toMap(BoaModule::getId, m -> m, (a, b) -> a));
|
||||||
|
|
||||||
|
// 每个模块节点(含 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 || !moduleMap.containsKey(parentId)) {
|
||||||
|
roots.add(node);
|
||||||
|
} else {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> children = (List<Map<String, Object>>) nodeMap.get(parentId).get("children");
|
||||||
|
children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持 SDK(用户中心)返回顺序,不再按 orderNumber 重排
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.yunhai.dormnotice.utils;
|
||||||
|
|
||||||
|
import com.yunhai.dormnotice.model.BasePageQuery;
|
||||||
|
import com.yunhai.dormnotice.model.BasePageResult;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页辅助类:BasePageQuery <-> Pageable,Page <-> BasePageResult。
|
||||||
|
*/
|
||||||
|
public class PageUtils {
|
||||||
|
|
||||||
|
private PageUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pageable toPageable(BasePageQuery query, Sort sort) {
|
||||||
|
int page = Math.max(query.getPageIndex() - 1, 0);
|
||||||
|
int size = Math.max(query.getPageSize(), 1);
|
||||||
|
if (sort == null) {
|
||||||
|
sort = Sort.unsorted();
|
||||||
|
}
|
||||||
|
return PageRequest.of(page, size, sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> BasePageResult<T> toResult(Page<T> page) {
|
||||||
|
return new BasePageResult<T>(
|
||||||
|
page.getContent(),
|
||||||
|
(int) page.getTotalElements(),
|
||||||
|
page.getNumber() + 1,
|
||||||
|
page.getSize(),
|
||||||
|
page.getTotalPages(),
|
||||||
|
page.hasNext()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.yunhai.dormnotice.utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求的用户上下文(ThreadLocal),参照 UserclassService 的自有 UserContext。
|
||||||
|
* <p>由 {@link com.yunhai.dormnotice.aspect.ControllerUserContextAspect} 在控制器执行前,
|
||||||
|
* 从基础平台 {@code BoaUserContext.get()}(或控制器 {@code BoaUser} 入参)复制填充,请求结束 {@link #clear()}。</p>
|
||||||
|
*
|
||||||
|
* <p>业务约定(决策 D4):实体只存数字 id,不存登录账号。
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #getUserId()} —— 平台用户数字 id;教师发布人存此值,家长(登录学生)也存此值作为 studentId;</li>
|
||||||
|
* <li>{@link #getRole()} —— 端分离视角:teacher 发布端 / parent 家长端(家长即登录学生账号,不单列),经 SetRole 落会话后由切面回填;</li>
|
||||||
|
* <li>{@link #getSchId()} —— 当前身份所属学校,多租户隔离。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class UserContext {
|
||||||
|
|
||||||
|
/** 家长端视角标识 */
|
||||||
|
public static final String ROLE_TEACHER = "teacher";
|
||||||
|
public static final String ROLE_PARENT = "parent";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前视角:teacher / parent(缺省 teacher) */
|
||||||
|
public static String getRole() {
|
||||||
|
UserInfo i = HOLDER.get();
|
||||||
|
return i == null || i.role == null ? ROLE_TEACHER : i.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isParent() {
|
||||||
|
return ROLE_PARENT.equals(getRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String role;
|
||||||
|
public Integer schId;
|
||||||
|
public Integer roleId;
|
||||||
|
public Integer type;
|
||||||
|
public String ip;
|
||||||
|
|
||||||
|
public UserInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserInfo(Integer userId, String userName, String role, Integer schId, Integer roleId, Integer type, String ip) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.userName = userName;
|
||||||
|
this.role = role;
|
||||||
|
this.schId = schId;
|
||||||
|
this.roleId = roleId;
|
||||||
|
this.type = type;
|
||||||
|
this.ip = ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
server:
|
||||||
|
port: 8105
|
||||||
|
|
||||||
|
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/dorm_notice?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
|
||||||
|
|
||||||
|
# 文件上传存储路径(Docker 部署时与 volume 挂载一致)
|
||||||
|
file:
|
||||||
|
upload-path: D:/data/upload
|
||||||
|
|
||||||
|
# 短信模块配置
|
||||||
|
sms:
|
||||||
|
enabled: true
|
||||||
|
module-id: notice
|
||||||
|
|
||||||
|
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
|
||||||
|
yh:
|
||||||
|
sso:
|
||||||
|
server-url-prefix: http://sso.yunhaixingtong.com
|
||||||
|
client-host-url: http://localhost:8105
|
||||||
|
allow-headers: all
|
||||||
|
allow-origins: all
|
||||||
|
url-pattern: /api/*
|
||||||
|
mobi-api-pattern: /mobiapi/*
|
||||||
|
app-auth:
|
||||||
|
enabled: true
|
||||||
|
# 本应用自身身份(客户端签名用)
|
||||||
|
app-key: 100011
|
||||||
|
app-secret: 992d257f83704b97a2d9821adffa98ca
|
||||||
|
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: dormnotice
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user