@@ -0,0 +1,5 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1 @@
|
||||
{"title":"【测试】curl-UTF8文件体","content":"<p>x</p>","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1,"publishType":"direct"}
|
||||
@@ -0,0 +1,17 @@
|
||||
# 2026-09-08 工作日志
|
||||
|
||||
## CampusNotice 启动异常修复
|
||||
- 现象:运行报"找不到主类"类异常,用户提及 `com.yunhai.campusnotice.CampusNoticeApplication`。
|
||||
- 根因:IDEA `workspace.xml` 的 RunManager 里存在一个配错的 Spring Boot Run Configuration
|
||||
`CampusNoticeApplication`,其 `SPRING_BOOT_MAIN_CLASS` 指向不存在的
|
||||
`com.yunhai.campusnotice.CampusNoticeApplication`,且未绑定 module;该配置被设为默认选中。
|
||||
项目实际启动类为 `com.yunhai.campusnotice.NoticeApplication`(pom mainClass、docker java -jar、代码三者一致)。
|
||||
- 修复:删除 workspace.xml 中错误的 `CampusNoticeApplication` 配置块,将 `selected` 与 list
|
||||
统一切到正确的 `NoticeApplication` 配置。
|
||||
- 提醒:若 IDEA 运行时编辑 workspace.xml 可能被覆盖,需重启 IDEA 让改动生效。
|
||||
|
||||
## CampusNotice IDE 编译 100 错误(依赖未同步)
|
||||
- 现象:IDEA Build 报 100 个"程序包 xxx 不存在"(lombok / spring / slf4j / javax.persistence / com.yunhai.platform.* 全部缺失)。
|
||||
- 根因:`campusnotice.iml` 的 orderEntry 只有 JDK + 源码目录,没有任何 Maven 依赖条目
|
||||
(isMavenModule=true 但依赖未写回)。Maven 命令行构建本身成功(target/campusnotice.jar 97MB 已生成,.m2 依赖齐全)。
|
||||
- 处理:纯 IDE 侧操作 —— Reload Maven Project(首选)/ Invalidate Caches,无需改代码。
|
||||
@@ -0,0 +1,50 @@
|
||||
## 目标
|
||||
框架/应用名从 notice / NoticeService 改为 **campusnotice / CampusNotice**(应用标识+显示名层级;Java 包名 `com.yunhai.notice` 与类名不动)。
|
||||
|
||||
## 仓库内修改(8 处)
|
||||
|
||||
1. **README.md**
|
||||
- 标题 `# NoticeService` → `# CampusNotice`
|
||||
- 两处远程仓库地址 `NoticeService.git` → `CampusNotice.git`
|
||||
|
||||
2. **pom.xml**
|
||||
- `<artifactId>notice</artifactId>` → `campusnotice`
|
||||
- `<name>Notice</name>` → `CampusNotice`
|
||||
- `<finalName>notice</finalName>` → `campusnotice`(产物变 campusnotice.jar;CI 用 `cp *.jar app.jar` 通配拷贝,镜像构建不受影响)
|
||||
- mainClass `com.yunhai.notice.NoticeApplication` 保持不变
|
||||
|
||||
3. **src/main/resources/bootstrap.yml**
|
||||
- `spring.application.name: notice` → `campusnotice`
|
||||
- 注释中 dataId 示例 `notice.yaml` → `campusnotice.yaml`
|
||||
|
||||
4. **src/main/resources/application.yml**
|
||||
- `module-id: notice` → `campusnotice`
|
||||
|
||||
5. **docker/application-prod.yml**
|
||||
- 注释中 dataId `notice.yaml` → `campusnotice.yaml`
|
||||
- `context-path: /notice` → `/campusnotice`
|
||||
- `yh.sso.client-host-url: http://base.yunhaixingtong.com/notice` → `.../campusnotice`(SSO 回调地址,必须跟随 context-path)
|
||||
- 库名 `bsch_notice` 不动
|
||||
|
||||
6. **docker/run-app.sh**
|
||||
- 注释 Notice → CampusNotice
|
||||
- `-jar /app/notice.jar` → `-jar /app/app.jar`(对齐 Dockerfile 实际路径;原 notice.jar 在当前镜像中不存在,属遗留错误)
|
||||
|
||||
7. **.gitea/workflows/build.yml**(projman 注入)
|
||||
- 镜像名 `docker-hosted/notice-service` → `docker-hosted/campusnotice`
|
||||
- webhook `"app":"notice"` → `"app":"campusnotice"`
|
||||
|
||||
8. **notice.iml → campusnotice.iml**,同步更新 `.idea/modules.xml` 引用
|
||||
|
||||
## 不改的
|
||||
Java 包/类名、数据库 `bsch_notice`、Nacos 分组 `OA_APP`、业务接口路径等域内 "notice" 字样。
|
||||
|
||||
## 仓库外需运维同步(不改则上线/CI 即断,仅提醒不在本次代码改动内)
|
||||
1. Nacos prod 命名空间新建 `dataId=campusnotice.yaml`(group 以 bootstrap.yml 的 `OA_APP` 为准;application-prod.yml 注释写的 NOTICE 与之不一致,顺带修正注释)
|
||||
2. Caddy 网关路由 `/notice` → `/campusnotice`(对外 URL 变化,调用方需知);CAS SSO 如有 client 地址白名单需更新
|
||||
3. projman 应用标识改/注册为 `campusnotice`,否则 webhook 建档失败
|
||||
4. Gitea 仓库改名 NoticeService → CampusNotice(README 中地址才有效)
|
||||
|
||||
## 验证
|
||||
- `mvn -DskipTests package` 构建通过,产物为 `target/campusnotice.jar`
|
||||
- grep 复查:除包名/域内字段外,不再残留旧标识(NoticeService、artifactId=notice、/notice 等)
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
# CampusNotice 接口 curl 测试脚本 v2
|
||||
# 用法: ./api-curl-test.sh [BASE] [PLATFORM]
|
||||
# BASE 默认 http://localhost:8101
|
||||
# PLATFORM 默认 manage(CAS 仅拦 /api/* 与 /mobiapi/*,manage 为匿名通道;生产环境应封死,见安全提醒)
|
||||
# 注意:中文 JSON 体必须走 UTF-8 文件(--data-binary @file),
|
||||
# 直接 -d '中文' 会被 Git Bash→curl.exe 的 argv 转换变成 GBK,服务端报
|
||||
# Invalid UTF-8 start byte(HttpMessageNotReadable → code=6),属测试工具问题。
|
||||
BASE="${1:-http://localhost:8101}"
|
||||
PF="${2:-manage}"
|
||||
U="$BASE/$PF"
|
||||
TMPB="$(dirname "$0")/.curl-body.json"
|
||||
PASS=0; BIZ=0; FAIL=0
|
||||
CREATED_IDS=()
|
||||
|
||||
req() { # req 名称 方法 路径 [json体]
|
||||
local name="$1" method="$2" path="$3" body="$4"
|
||||
local args=(-s --max-time 15 -w "\n%{http_code}" -H "Accept: application/json")
|
||||
if [ -n "$body" ]; then
|
||||
printf '%s' "$body" > "$TMPB" # UTF-8 原样字节落盘
|
||||
args+=(-X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")")
|
||||
elif [ "$method" = "POST" ]; then
|
||||
args+=(-X POST)
|
||||
fi
|
||||
local out; out=$(curl "${args[@]}" "$U$path" 2>/dev/null)
|
||||
local http="${out##*$'\n'}"; local json="${out%$'\n'*}"
|
||||
local parsed; parsed=$(node -e "
|
||||
try{const j=JSON.parse(process.argv[1]);
|
||||
const d=j.data;
|
||||
const ds=d===null||d===undefined?'null':Array.isArray(d)?'array['+d.length+']':typeof d==='object'?'{'+Object.keys(d).slice(0,4).join(',')+'}':String(d).slice(0,30);
|
||||
console.log([j.code,(j.msg||'').slice(0,46),ds].join('\u0001'));
|
||||
}catch(e){console.log(['-','-','-'].join('\u0001'))}" "$json" 2>/dev/null)
|
||||
local code msg data
|
||||
code=$(echo "$parsed" | cut -d$'\001' -f1)
|
||||
msg=$(echo "$parsed" | cut -d$'\001' -f2)
|
||||
data=$(echo "$parsed" | cut -d$'\001' -f3)
|
||||
local st
|
||||
if [ "$code" = "0" ]; then PASS=$((PASS+1)); st="✅PASS";
|
||||
elif [ "$code" = "-" ] || [ "$http" != "200" ]; then FAIL=$((FAIL+1)); st="❌FAIL";
|
||||
else BIZ=$((BIZ+1)); st="🟡BIZ"; fi
|
||||
printf "%-26s | HTTP %s | code=%-3s | msg=%-46s | data=%s\n" "$name" "$http" "$code" "$msg" "$data"
|
||||
}
|
||||
|
||||
extract_id() { # 从最近一次输出无法跨函数取值,改为调用方直接用 node 从 curl 结果提取
|
||||
node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).data ?? '')}catch(e){console.log('')}" "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
echo "================ CampusNotice curl 矩阵 (BASE=$BASE PF=$PF) ================"
|
||||
|
||||
echo "---- A. 静态(直连 BASE,不经 platform 前缀) ----"
|
||||
for p in "/" "/api-test.html"; do
|
||||
http=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$BASE$p")
|
||||
if [ "$http" = "200" ]; then PASS=$((PASS+1)); printf "%-26s | HTTP %s\n" "静态 $p" "$http"; else FAIL=$((FAIL+1)); printf "%-26s | HTTP %s ❌\n" "静态 $p" "$http"; fi
|
||||
done
|
||||
http=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$BASE/api/notice/Statis")
|
||||
if [ "$http" = "302" ]; then PASS=$((PASS+1)); printf "%-26s | HTTP 302 → CAS 登录墙(符合预期)\n" "/api 登录墙对照"; else FAIL=$((FAIL+1)); printf "%-26s | HTTP $http ❌(预期302)\n" "/api 登录墙对照"; fi
|
||||
|
||||
echo "---- B. 平台/基础(匿名语义) ----"
|
||||
req "GetUserInfo" GET "/base/GetUserInfo"
|
||||
req "GetUserMod(匿名预期拒)" GET "/base/GetUserMod"
|
||||
req "GetSchoolAll" GET "/base/GetSchoolAll"
|
||||
req "GetGroupUser(未接入桩)" GET "/base/GetGroupUser?gid=1&role=2"
|
||||
|
||||
echo "---- C. 通知查询 ----"
|
||||
req "notice/List" GET "/notice/List?pageIndex=1&pageSize=3"
|
||||
req "notice/SendList" GET "/notice/SendList?pageIndex=1&pageSize=3&status=1&scope=user"
|
||||
req "notice/Statis" GET "/notice/Statis"
|
||||
req "notice/GetNextPrev" GET "/notice/GetNextPrev?serid=1&type=0"
|
||||
req "notice/DraftList" GET "/notice/DraftList?pageIndex=1&pageSize=3"
|
||||
req "notice/ApproverSuggest" GET "/notice/ApproverSuggest"
|
||||
req "ApprovalList(pending)" GET "/notice/ApprovalList?auditState=pending&pageIndex=1&pageSize=3"
|
||||
req "ApprovalList(reviewed)" GET "/notice/ApprovalList?auditState=reviewed&pageIndex=1&pageSize=3"
|
||||
|
||||
echo "---- D. 推送范围(匿名预期拒) ----"
|
||||
req "notice/ScopeTree" GET "/notice/ScopeTree"
|
||||
req "notice/ScopePreview" GET "/notice/ScopePreview?scopes=%5B%7B%22scopeType%22%3A1%2C%22scopeId%22%3A0%2C%22scopeName%22%3A%22%E5%85%A8%E6%A0%A1%22%7D%5D"
|
||||
|
||||
echo "---- E. 写链路(UTF-8 文件体,测后自清理) ----"
|
||||
printf '%s' '{"title":"【测试】curl直发","content":"<p>curl</p>","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1,"publishType":"direct"}' > "$TMPB"
|
||||
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Create")
|
||||
echo "$resp" > "$TMPB.out"
|
||||
ID_DIRECT=$(extract_id "$TMPB.out")
|
||||
if [ -n "$ID_DIRECT" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | id=%s ✅\n" "Create(direct)" "$ID_DIRECT"; else FAIL=$((FAIL+1)); printf "%-26s | ❌ 未返回id\n" "Create(direct)"; fi
|
||||
CREATED_IDS+=("$ID_DIRECT")
|
||||
|
||||
printf '%s' '{"title":"【测试】curl草稿","content":"<p>curl</p>","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1}' > "$TMPB"
|
||||
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/SaveDraft")
|
||||
echo "$resp" > "$TMPB.out"
|
||||
ID_DRAFT=$(extract_id "$TMPB.out")
|
||||
if [ -n "$ID_DRAFT" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | id=%s ✅\n" "SaveDraft" "$ID_DRAFT"; else FAIL=$((FAIL+1)); printf "%-26s | ❌ 未返回id\n" "SaveDraft"; fi
|
||||
CREATED_IDS+=("$ID_DRAFT")
|
||||
|
||||
if [ -n "$ID_DRAFT" ]; then
|
||||
printf '%s' "{\"serid\":$ID_DRAFT,\"approverAccounts\":[\"lijing\"],\"opinion\":\"请审批\"}" > "$TMPB"
|
||||
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Submit")
|
||||
code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp")
|
||||
if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | 草稿→待审批 ✅\n" "Submit"; else BIZ=$((BIZ+1)); printf "%-26s | code=%s 🟡\n" "Submit" "$code"; fi
|
||||
fi
|
||||
req "Approve(匿名预期拒)" POST "/notice/Approve?serid=${ID_DRAFT:-0}&opinion=x"
|
||||
req "Reject(匿名预期拒)" POST "/notice/Reject?serid=${ID_DRAFT:-0}&opinion=x"
|
||||
|
||||
echo "---- F. 其他模块 ----"
|
||||
req "dict/ManageList" GET "/dict/ManageList"
|
||||
req "template/Typelist" GET "/template/Typelist"
|
||||
req "contacts/GetGroup" GET "/contacts/GetGroup"
|
||||
req "sms/GetSchool" GET "/sms/GetSchool"
|
||||
|
||||
echo "---- G. 附件 ----"
|
||||
f="$(dirname "$0")/.curl-upload-sample.txt"; echo "curl upload sample $(date +%s)" > "$f"
|
||||
req "base/FileUpload" POST "/base/FileUpload" "" "file=@$(cygpath -w "$f" 2>/dev/null || echo "$f")"
|
||||
rm -f "$f"
|
||||
|
||||
echo "---- H. 清理测试数据 ----"
|
||||
if [ "${#CREATED_IDS[@]}" -gt 0 ]; then
|
||||
ids=$(IFS=,; echo "${CREATED_IDS[*]}")
|
||||
printf '%s' "[$ids]" > "$TMPB"
|
||||
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Delete")
|
||||
code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp")
|
||||
if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | ids=[$ids] 已清理 ✅\n" "Delete清理"; else FAIL=$((FAIL+1)); printf "%-26s | code=%s ❌\n" "Delete清理" "$code"; fi
|
||||
fi
|
||||
rm -f "$TMPB" "$TMPB.out"
|
||||
|
||||
echo "================ 统计: PASS=$PASS BIZ(业务拒绝/环境预期)=$BIZ FAIL=$FAIL ================"
|
||||
echo "提醒: /manage 匿名可访问全部业务接口——CAS 仅拦 /api/* 与 /mobiapi/*,生产环境务必封死(安全项)。"
|
||||
@@ -0,0 +1,52 @@
|
||||
# CampusNotice 生产配置(线上以 nacos 的 campusnotice.yaml 为准,本文件是 nacos 不可达时的兜底)
|
||||
# 真正上线请在 Nacos 新建 dataId=campusnotice.yaml、group=OA_APP 的配置覆盖以下项
|
||||
# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
|
||||
server:
|
||||
port: 8070
|
||||
servlet:
|
||||
# 对外经 caddy 暴露为 http://base.yunhaixingtong.com/campusnotice
|
||||
context-path: /campusnotice
|
||||
|
||||
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;库名为 bsch_notice(需先在 mysql8 上建库)
|
||||
url: jdbc:mysql://mysql8:3306/bsch_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/campusnotice
|
||||
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
|
||||
# CampusNotice 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml)
|
||||
# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到
|
||||
# 镜像内烤好的 application-prod.yml 兜底默认。
|
||||
set -e
|
||||
|
||||
echo "[run-app] starting CampusNotice (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..."
|
||||
exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/app.jar \
|
||||
--spring.profiles.active=prod
|
||||
@@ -0,0 +1,198 @@
|
||||
<?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>campusnotice</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<name>CampusNotice</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-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>campusnotice</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.campusnotice.CampusNoticeApplication</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>
|
||||
<snapshots><enabled>true</enabled></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,30 @@
|
||||
-- =====================================================================
|
||||
-- 修复:campus_notice 库字符集 latin1 → utf8mb4(2026-09-09 接口检测发现)
|
||||
-- 现象:所有含中文的写入(notice Create/SaveDraft/Forward/Edit、审批人姓名、
|
||||
-- 审批意见等)报 Incorrect string value → 全局兜底为「内部服务错误」。
|
||||
-- 根因:建库未指定字符集(latin1_swedish_ci),Hibernate ddl-auto=update
|
||||
-- 建表继承库默认,全部 14 张表均为 latin1。
|
||||
-- 修复:在线转换(存量数据仅 ASCII,转换无损);应用无需重启。
|
||||
-- DBA 建议:生产建库统一 utf8mb4_general_ci;新表继承库默认后即为 utf8mb4。
|
||||
-- =====================================================================
|
||||
|
||||
ALTER DATABASE campus_notice CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
|
||||
ALTER TABLE campus_notice.notice CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_read CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_receiver CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_push_scope CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_approval CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_approver_memory CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.sys_dict CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.template_type CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.notice_template CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.up_file CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.contacts_group CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.contacts_group_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.sms_module CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE campus_notice.sms_school_config CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- 核查(预期全部 utf8mb4):
|
||||
-- SELECT table_name, table_collation FROM information_schema.TABLES
|
||||
-- WHERE table_schema='campus_notice' ORDER BY table_name;
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.campusnotice;
|
||||
|
||||
import com.alibaba.fastjson.parser.ParserConfig;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.yunhai.campusnotice")
|
||||
@EnableTransactionManagement
|
||||
@EnableScheduling
|
||||
public class CampusNoticeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 开启 fastjson 安全模式
|
||||
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||
SpringApplication.run(CampusNoticeApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yunhai.campusnotice.common.listener;
|
||||
|
||||
import com.yunhai.campusnotice.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.campusnotice.common.validation;
|
||||
|
||||
/**
|
||||
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||
*/
|
||||
public interface ValidationGroups {
|
||||
interface Create {}
|
||||
interface Update {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.yunhai.campusnotice.config;
|
||||
|
||||
/**
|
||||
* 全局路径常量。与 UserBase 一致:API 统一挂在 /{platform}/ 之下(platform = api / manage / mobiapi)。
|
||||
*/
|
||||
public enum Config {
|
||||
Inst;
|
||||
|
||||
public static final String API_PATH = "/{platform}"; // manage / api / mobiapi,前面有/后面无/,拼接子路径时用"/"分隔
|
||||
public static final String OPEN_API_PATH = "/oapi";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.yunhai.campusnotice.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Jackson 时间序列化全局约定(对齐旧 .NET 版 "yyyy-MM-dd HH:mm:ss"):
|
||||
* 接口出入参中的 LocalDateTime/LocalDate 统一使用该格式,避免 ISO 的 "T" 分隔符
|
||||
* (如 2026-09-01T16:38:40)直接暴露给前端表格。
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||
return builder -> builder
|
||||
.serializers(
|
||||
new LocalDateTimeSerializer(DATE_TIME),
|
||||
new LocalDateSerializer(DATE))
|
||||
.deserializers(
|
||||
new LocalDateTimeDeserializer(DATE_TIME),
|
||||
new LocalDateDeserializer(DATE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.campusnotice.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,122 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.campusnotice.service.IClassService;
|
||||
import com.yunhai.campusnotice.service.IFileService;
|
||||
import com.yunhai.campusnotice.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 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 IClassService classService;
|
||||
|
||||
@Autowired
|
||||
private IFileService fileService;
|
||||
|
||||
// ============================ 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, "notice");
|
||||
}
|
||||
|
||||
// ============================ 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("getStuTree")
|
||||
public List<SchTreeNodeDto> getStuTree(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
return classService.getStuTree(schId);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
// ============================ 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.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.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,73 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.service.IDictService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典接口(发文部门域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/dict/"}(原 base/ 前缀已修正)。
|
||||
* 写操作一律 POST(原 DelDept/OrdbyDicIds 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@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", defaultValue = "0") Integer type) {
|
||||
return dictService.getSysDic(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表。
|
||||
*/
|
||||
@GetMapping("ManageList")
|
||||
public List<Map<String, Object>> manageList(@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
return dictService.getDictMangeList(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加发文单位。
|
||||
*/
|
||||
@PostMapping("Add")
|
||||
public void add(@RequestParam("name") String name) {
|
||||
dictService.addDeptDic(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新发文单位。
|
||||
*/
|
||||
@PostMapping("Update")
|
||||
public void update(
|
||||
@RequestParam("SerID") Integer serID,
|
||||
@RequestParam("Name") String name,
|
||||
@RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable) {
|
||||
dictService.updateDeptDic(serID, name, enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除发文单位(原 GET DelDept,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestParam("serid") Integer serID) {
|
||||
dictService.delDept(serID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发文单位排序(原 GET OrdbyDicIds,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("Sort")
|
||||
public void sort(@RequestParam("ids") String ids) {
|
||||
dictService.ordbyDicIds(ids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileVo;
|
||||
import com.yunhai.campusnotice.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, "notice");
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(多文件,返回数组)。
|
||||
* 前端: POST file/UploadImage (form-data, accept=image/*)
|
||||
*/
|
||||
@PostMapping("UploadImage")
|
||||
public List<UpFileVo> uploadImage(@RequestParam("file") MultipartFile[] files) {
|
||||
return fileService.uploadImages(files, "notice");
|
||||
}
|
||||
/**
|
||||
* 文件下载(浏览器直接 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("notice", topicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量绑定附件到通知。
|
||||
*/
|
||||
@PostMapping("Bind")
|
||||
public void bind(@RequestParam("fileIds") List<Integer> fileIds,
|
||||
@RequestParam("topicId") Integer topicId) {
|
||||
fileService.bindToTopic(fileIds, topicId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.common.validation.ValidationGroups;
|
||||
import com.yunhai.campusnotice.model.BasePageResult;
|
||||
import com.yunhai.campusnotice.model.notice.*;
|
||||
import com.yunhai.campusnotice.service.INoticeService;
|
||||
import com.yunhai.campusnotice.utils.OtherUtil;
|
||||
import com.yunhai.campusnotice.utils.PageUtils;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.print.attribute.standard.MediaSize;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知公告接口(合并 NoticeApiController + 旧 NoticeController)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/notice/"}(platform = api / manage / mobiapi)。
|
||||
* 方法动词式 PascalCase(对齐 UserBase);写操作一律 POST。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/notice/")
|
||||
public class NoticeController {
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
// ============================ 收件箱列表 ============================
|
||||
|
||||
/**
|
||||
* 已收通知列表(用户端)。
|
||||
*/
|
||||
@GetMapping("List")
|
||||
public NoticePageResult<NoticeVo> list(NoticeQuery query,
|
||||
@SortDefault(sort = {"topmost", "createTime"},
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
// ============================ 已发/全部通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已发/全部通知列表(管理端)。type=user 只看自己。
|
||||
*/
|
||||
@GetMapping("SendList")
|
||||
public BasePageResult<NoticeVo> sendList(NoticeQuery query,
|
||||
@SortDefault(sort = "createTime",
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.adminList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
// ============================ 查看详情(标读) ============================
|
||||
|
||||
/**
|
||||
* 查看通知详情(type=1 普通查看标记已读, type=0 编辑模式不标记已读)。
|
||||
*/
|
||||
@GetMapping("Detail")
|
||||
public NoticeDetailVo detail(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||
return noticeService.detailMap(serid, type);
|
||||
}
|
||||
|
||||
// ============================ 编辑回显(不标读) ============================
|
||||
|
||||
/**
|
||||
* 获取通知详情(编辑/转发用,不标记已读)。
|
||||
*/
|
||||
@GetMapping("GetDetail")
|
||||
public NoticeDetailVo getDetail(@RequestParam("serid") Integer serid) {
|
||||
return noticeService.getDetailMap(serid);
|
||||
}
|
||||
|
||||
// ============================ 新建 ============================
|
||||
|
||||
/**
|
||||
* 新建通知(JSON body)。fileIds 为附件ID逗号串,smsFlag 为短信标记。
|
||||
*/
|
||||
@PostMapping("Create")
|
||||
public Integer create(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) {
|
||||
return noticeService.publish(request);
|
||||
}
|
||||
|
||||
// ============================ 编辑 ============================
|
||||
|
||||
/**
|
||||
* 编辑通知(JSON body,含 id)。
|
||||
*/
|
||||
@PostMapping("Edit")
|
||||
public void edit(@RequestBody @Validated(ValidationGroups.Update.class) NoticeRequest request) {
|
||||
noticeService.edit(request.getSourceId() != null ? request.getSourceId() : 0, request);
|
||||
}
|
||||
|
||||
// ============================ 批量删除 ============================
|
||||
|
||||
/**
|
||||
* 批量删除通知(ids 数组)。
|
||||
*/
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestBody List<Integer> ids) {
|
||||
noticeService.deleteBatch(ids);
|
||||
}
|
||||
|
||||
// ============================ 撤回 ============================
|
||||
|
||||
/**
|
||||
* 撤回通知。
|
||||
*/
|
||||
@PostMapping("UserPull")
|
||||
public void userPull(@RequestParam("serid") Integer serid) {
|
||||
noticeService.pull(serid);
|
||||
}
|
||||
|
||||
// ============================ 置顶 ============================
|
||||
|
||||
/**
|
||||
* 置顶/取消置顶。
|
||||
*/
|
||||
@PostMapping("SetTopmost")
|
||||
public void setTopmost(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "topmost", defaultValue = "1") Integer topmost) {
|
||||
noticeService.setTopmost(serid, topmost);
|
||||
}
|
||||
|
||||
// ============================ 追加接收人 ============================
|
||||
|
||||
/**
|
||||
* 追加接收人。
|
||||
*/
|
||||
@PostMapping("ReceiveUsers")
|
||||
public void receiveUsers(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("users") String users) {
|
||||
noticeService.receiveUsers(serid, users);
|
||||
}
|
||||
|
||||
// ============================ 催收 ============================
|
||||
|
||||
/**
|
||||
* 催收未读。
|
||||
*/
|
||||
@PostMapping("Expedite")
|
||||
public void expedite(@RequestParam("serid") Integer serid) {
|
||||
noticeService.expedite(serid);
|
||||
}
|
||||
|
||||
// ============================ 一键已读 ============================
|
||||
|
||||
/**
|
||||
* 一键全部已读(POST 化,原为 GET 写操作)。
|
||||
*/
|
||||
@PostMapping("InitializeRed")
|
||||
public InitializeRedVo initializeRed() {
|
||||
return noticeService.initializeRedResult();
|
||||
}
|
||||
|
||||
// ============================ 阅读情况 ============================
|
||||
|
||||
/**
|
||||
* 阅读情况(按已读/未读分组)。
|
||||
*/
|
||||
@GetMapping("ReadDetail")
|
||||
public List<Map<String, Object>> readDetail(@RequestParam("serid") Integer serid) {
|
||||
return noticeService.getReadDetailGrouped(serid);
|
||||
}
|
||||
|
||||
// ============================ 上一篇/下一篇 ============================
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇通知ID。
|
||||
*/
|
||||
@GetMapping("GetNextPrev")
|
||||
public Map<String, Object> getNextPrev(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("type") String type) {
|
||||
return noticeService.getNextPrev(serid, type);
|
||||
}
|
||||
|
||||
// ============================ 附件打包下载 ============================
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件(打包 zip)。
|
||||
*/
|
||||
@GetMapping("AllFileDown")
|
||||
public void allFileDown(@RequestParam("dataid") Integer dataid,
|
||||
HttpServletResponse response) {
|
||||
noticeService.downloadAllFiles(dataid, response);
|
||||
}
|
||||
|
||||
// ============================ 统计 ============================
|
||||
|
||||
/**
|
||||
* 通知统计。
|
||||
*/
|
||||
@GetMapping("Statis")
|
||||
public NoticeStatisticsVo statis(
|
||||
@RequestParam(value = "starttime", required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
|
||||
@RequestParam(value = "endtime", required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) {
|
||||
return noticeService.statistics(new NoticeQuery(), start, end);
|
||||
}
|
||||
|
||||
// ============================ 转发(合并旧 REST 壳 forward) ============================
|
||||
|
||||
/**
|
||||
* 转发通知(基于源通知创建新通知)。sourceId 在 request body 中传递。
|
||||
*/
|
||||
@PostMapping("Forward")
|
||||
public Integer forward(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) {
|
||||
return noticeService.forward(request.getSourceId(), request);
|
||||
}
|
||||
|
||||
// ============================ 推送范围树(新版发布表单,改造方案 3.1) ============================
|
||||
|
||||
/**
|
||||
* 当前学校学员推送范围树:全校根 + 学段→年级→班级(数据源 bschclass),
|
||||
* 供表单「推送范围」胶囊按钮渲染。节点结构:{scopeType, scopeId, name, children}。
|
||||
*/
|
||||
@GetMapping("ScopeTree")
|
||||
public List<Map<String, Object>> scopeTree() {
|
||||
return noticeService.scopeTree();
|
||||
}
|
||||
|
||||
// ============================ 推送范围预估 ============================
|
||||
|
||||
/**
|
||||
* 按已选范围预估接收人数(学员 = 范围展开去重账号数 + 教师 = receiveList 定向名单数)。
|
||||
* scopes 为 JSON 数组:[{"scopeType":3,"scopeId":5,"scopeName":"初一"}]
|
||||
*/
|
||||
@GetMapping("ScopePreview")
|
||||
public ScopePreviewVo scopePreview(@RequestParam(value = "scopes", required = false) String scopes,
|
||||
@RequestParam(value = "receiveList", required = false) String receiveList) {
|
||||
return noticeService.scopePreview(parseScopes(scopes), receiveList);
|
||||
}
|
||||
|
||||
// ============================ 存为草稿 ============================
|
||||
|
||||
/**
|
||||
* 存为草稿(status=0):不圈人、不发短信、不落接收人,仅存内容 + 范围快照 + 附件。
|
||||
*/
|
||||
@PostMapping("SaveDraft")
|
||||
public Integer saveDraft(@RequestBody NoticeRequest request) {
|
||||
return noticeService.saveDraft(request);
|
||||
}
|
||||
|
||||
// ============================ 草稿箱列表 ============================
|
||||
|
||||
/**
|
||||
* 草稿箱列表(status=0 本人草稿):重要程度 type/typeDesc、定时标注与 timingSendTime。
|
||||
*/
|
||||
@GetMapping("DraftList")
|
||||
public BasePageResult<NoticeVo> draftList(NoticeQuery query,
|
||||
@SortDefault(sort = "updateTime",
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.draftList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
// ============================ 已发送阅读回执 ============================
|
||||
|
||||
/**
|
||||
* 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照),
|
||||
* 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。
|
||||
*/
|
||||
@GetMapping("ReadReceipt")
|
||||
public ReadReceiptVo readReceipt(@RequestParam("serid") Integer serid) {
|
||||
return noticeService.readReceipt(serid);
|
||||
}
|
||||
|
||||
// ============================ 审批人记忆回显 ============================
|
||||
|
||||
/**
|
||||
* 审批人记忆回显:当前发布人最近一次所选审批人组合(无记忆返回空)。
|
||||
*/
|
||||
@GetMapping("ApproverSuggest")
|
||||
public ApproverSuggestVo approverSuggest() {
|
||||
return noticeService.approverSuggest();
|
||||
}
|
||||
|
||||
// ============================ 审批教师绑定(教师树ID→姓名/头像) ============================
|
||||
|
||||
/**
|
||||
* 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。
|
||||
* ids=101,102(教师树叶子节点ID);schId 缺省取当前登录人学校;
|
||||
* 未命中的ID保留、name 为 null,由前端提示。
|
||||
*/
|
||||
@GetMapping("ApproverResolve")
|
||||
public List<ApproverResolveVo> approverResolve(
|
||||
@RequestParam(value = "ids", required = false) String ids,
|
||||
@RequestParam(value = "schId", required = false) Integer schId) {
|
||||
if (ids == null || ids.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "ids 不允许为空");
|
||||
}
|
||||
return noticeService.approverResolve(Arrays.asList(OtherUtil.convertToArray(ids)), schId);
|
||||
}
|
||||
|
||||
// ============================ 提交审批 ============================
|
||||
|
||||
/**
|
||||
* 提交审批(status: 0/3→2):发布人从教师树选定审批教师(可多选,或签),
|
||||
* 写 notice_approval 提交行并 upsert 审批人记忆表。
|
||||
*/
|
||||
@PostMapping("Submit")
|
||||
public void submit(@RequestBody SubmitRequest request) {
|
||||
noticeService.submit(request.getSerid(), request.getApproverTeaIds(), request.getOpinion());
|
||||
}
|
||||
|
||||
// ============================ 审批通过 ============================
|
||||
|
||||
/**
|
||||
* 审批通过(或签):任一审批人通过即 status→1 并触发真实发布;
|
||||
* 被他人先审时返回带实际处理人/时间的冲突提示。
|
||||
*/
|
||||
@PostMapping("Approve")
|
||||
public void approve(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "opinion", required = false) String opinion) {
|
||||
noticeService.approve(serid, opinion);
|
||||
}
|
||||
|
||||
// ============================ 审批驳回 ============================
|
||||
|
||||
/**
|
||||
* 审批驳回(或签):任一审批人驳回即 status→3,意见必填;发布人可改后重新提交。
|
||||
*/
|
||||
@PostMapping("Reject")
|
||||
public void reject(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("opinion") String opinion) {
|
||||
noticeService.reject(serid, opinion);
|
||||
}
|
||||
|
||||
// ============================ 审批列表(待审/已审合一) ============================
|
||||
|
||||
/**
|
||||
* 审批列表:按审批人快照含当前账号过滤(schId 隔离)。
|
||||
* auditState=pending(默认) 待审 / reviewed 已审(含或签下他人已处理,带实际处理人)。
|
||||
*/
|
||||
@GetMapping("ApprovalList")
|
||||
public BasePageResult<ApprovalListVo> approvalList(
|
||||
@RequestParam(value = "auditState", defaultValue = "pending") String auditState,
|
||||
NoticeQuery query,
|
||||
@SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.approvalList(auditState, query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
/** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */
|
||||
private List<PushScopeItem> parseScopes(String scopes) {
|
||||
if (scopes == null || scopes.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new ObjectMapper().readValue(scopes, new TypeReference<List<PushScopeItem>>() {
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "scopes 参数格式错误,应为 JSON 数组:[{\"scopeType\":3,\"scopeId\":5}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.service.ISmsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信设置接口(短信域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。
|
||||
* 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/sms/")
|
||||
public class SmsController {
|
||||
|
||||
@Autowired
|
||||
private ISmsService smsService;
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
*/
|
||||
@GetMapping("GetModule")
|
||||
public List<Map<String, Object>> getModule() {
|
||||
return smsService.getModuleList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学校父类组列表。
|
||||
*/
|
||||
@GetMapping("GetGroup")
|
||||
public List<Map<String, Object>> getGroup() {
|
||||
return smsService.getSchParentGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取学校短信开关列表。
|
||||
*/
|
||||
@GetMapping("GetSchoolSms")
|
||||
public Map<String, Object> getSchoolSms(
|
||||
@RequestParam(value = "schpid", required = false) String schpid,
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "SchName", required = false, defaultValue = "") String schName) {
|
||||
return smsService.getSchIsSmsList(pageSize, pageIndex, schName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学校列表。
|
||||
*/
|
||||
@GetMapping("GetSchool")
|
||||
public List<Map<String, Object>> getSchool() {
|
||||
return smsService.getSchList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单校单模块短信开关。
|
||||
*/
|
||||
@PostMapping("SetSchoolSms")
|
||||
public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) {
|
||||
smsService.setSchSmsFromJson(itemsModuJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("SetAllSchoolSms")
|
||||
public void setAllSchoolSms(
|
||||
@RequestParam("SchSerID") Integer schSerID,
|
||||
@RequestParam("IsSms") Boolean isSms) {
|
||||
smsService.setSchSmsAll(schSerID, isSms);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("SetSchoolModuleSms")
|
||||
public void setSchoolModuleSms(
|
||||
@RequestParam("SchSerID") String schSerID,
|
||||
@RequestParam("Module") String module,
|
||||
@RequestParam("IsSms") Boolean isSms) {
|
||||
smsService.setSchModuleSms(schSerID, module, isSms);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.yunhai.campusnotice.controller.api;
|
||||
|
||||
import com.yunhai.campusnotice.config.Config;
|
||||
import com.yunhai.campusnotice.service.ITemplateService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知模板接口(模板域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。
|
||||
* 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/template/")
|
||||
public class TemplateController {
|
||||
|
||||
@Autowired
|
||||
private ITemplateService templateService;
|
||||
|
||||
/**
|
||||
* 获取模板类型列表。
|
||||
*/
|
||||
@GetMapping("Typelist")
|
||||
public List<Map<String, Object>> getTypelist() {
|
||||
return templateService.getTypelist();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增/编辑模板类型(原 GET EditType,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("TypeEdit")
|
||||
public void typeEdit(
|
||||
@RequestParam("typeid") Integer typeid,
|
||||
@RequestParam("TypeName") String typeName) {
|
||||
templateService.editType(typeid, typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板类型(原 GET DelType,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("TypeDelete")
|
||||
public void typeDelete(@RequestParam("typeid") Integer typeid) {
|
||||
templateService.delType(typeid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取模板列表。
|
||||
*/
|
||||
@GetMapping("List")
|
||||
public Map<String, Object> list(
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "typeid", defaultValue = "0") Integer typeid) {
|
||||
return templateService.getTemplatelist(pageSize, pageIndex, typeid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增/编辑模板。
|
||||
*/
|
||||
@PostMapping("TemplateEdit")
|
||||
public void templateEdit(
|
||||
@RequestParam(value = "SerID", required = false) Integer serID,
|
||||
@RequestParam("Title") String title,
|
||||
@RequestParam("Contents") String contents,
|
||||
@RequestParam(value = "Type", required = false) Integer type,
|
||||
@RequestParam(value = "Cover", required = false) Integer cover) {
|
||||
templateService.editTemplate(serID, title, contents, type, cover);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板(原 GET DelTemplate,已 POST 化)。
|
||||
*/
|
||||
@PostMapping("TemplateDelete")
|
||||
public void templateDelete(@RequestParam("SerID") Integer serid) {
|
||||
templateService.delTemplate(serid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个模板详情。
|
||||
*/
|
||||
@GetMapping("TemplateFirst")
|
||||
public Map<String, Object> templateFirst(@RequestParam("serid") Integer serid) {
|
||||
return templateService.getTemplateFist(serid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import com.yunhai.campusnotice.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.campusnotice.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;
|
||||
|
||||
/** 创建人账号 */
|
||||
@Column(name = "creator_account", length = 64)
|
||||
private String creatorAccount;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.yunhai.campusnotice.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) */
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Integer userId;
|
||||
|
||||
/** 用户账号 */
|
||||
@Column(name = "user_account", length = 64)
|
||||
private String userAccount;
|
||||
|
||||
/** 用户姓名 */
|
||||
@Column(name = "user_name", length = 64)
|
||||
private String userName;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.yunhai.campusnotice.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;
|
||||
|
||||
/**
|
||||
* 校园公告主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。
|
||||
* <p>
|
||||
* 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link NoticeRead},
|
||||
* 不存在阅读记录即视为未读,从而无需在发通知时枚举全校用户预插接收行。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class Notice extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 通知标题
|
||||
*/
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。
|
||||
*/
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/**
|
||||
* 通知内容
|
||||
*/
|
||||
@Column(name = "content", columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.campusnotice.enums.NoticeTypeEnum})
|
||||
*/
|
||||
@Column(name = "type", nullable = false)
|
||||
private Integer type = 1;
|
||||
|
||||
/**
|
||||
* 发布人登录账号
|
||||
*/
|
||||
@Column(name = "publisher_account", length = 64)
|
||||
private String publisherAccount;
|
||||
|
||||
/**
|
||||
* 发布人姓名
|
||||
*/
|
||||
@Column(name = "publisher_name", length = 64)
|
||||
private String publisherName;
|
||||
|
||||
/**
|
||||
* 所属分类ID(原 DeptID)
|
||||
*/
|
||||
@Column(name = "dept_id", length = 64)
|
||||
private String deptId;
|
||||
|
||||
/**
|
||||
* 所属分类名称
|
||||
*/
|
||||
@Column(name = "dept_name", length = 128)
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 状态机值见 {@link com.yunhai.campusnotice.enums.NoticeStatusEnum}:
|
||||
* 0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回。
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = 0;
|
||||
|
||||
/**
|
||||
* 是否置顶:0 否 / 1 是(原 topmost)
|
||||
*/
|
||||
@Column(name = "topmost")
|
||||
private Integer topmost = 0;
|
||||
|
||||
// ==================== 迁移补全字段(对应 C# Notify_Message) ====================
|
||||
|
||||
/**
|
||||
* 接收人列表(JSON 数组,如 ["account1","account2"] 或接收组ID列表)。
|
||||
* 对应 C# Notify_Message.ReceiveList
|
||||
*/
|
||||
@Column(name = "receive_list", columnDefinition = "LONGTEXT")
|
||||
private String receiveList;
|
||||
|
||||
/**
|
||||
* 密码发送(对应 C# Notify_Message.PwdSend)
|
||||
*/
|
||||
@Column(name = "pwd_send", length = 255)
|
||||
private String pwdSend;
|
||||
|
||||
/**
|
||||
* 抄送人列表(对应 C# Notify_Message.ChaoSend)
|
||||
*/
|
||||
@Column(name = "chao_send", length = 2000)
|
||||
private String chaoSend;
|
||||
|
||||
/**
|
||||
* 已读数(发送时初始化为 0,也可由 NoticeRead 表 COUNT 实时计算)。
|
||||
* 对应 C# Notify_Message.readcount
|
||||
*/
|
||||
@Column(name = "read_count")
|
||||
private Integer readCount = 0;
|
||||
|
||||
/**
|
||||
* 应读数(接收人总数)。对应 C# Notify_Message.allowcount
|
||||
*/
|
||||
@Column(name = "allow_count")
|
||||
private Integer allowCount = 0;
|
||||
|
||||
/**
|
||||
* 父通知ID(转发时关联原通知)。对应 C# Notify_Message.fkid
|
||||
*/
|
||||
@Column(name = "parent_id")
|
||||
private Integer parentId;
|
||||
|
||||
/**
|
||||
* 回复ID。对应 C# Notify_Message.reid
|
||||
*/
|
||||
@Column(name = "reply_id", length = 255)
|
||||
private String replyId;
|
||||
|
||||
/**
|
||||
* 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull
|
||||
*/
|
||||
@Column(name = "is_pulled")
|
||||
private Integer isPulled = 0;
|
||||
|
||||
/**
|
||||
* 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime
|
||||
*/
|
||||
@Column(name = "timing_send_time")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/**
|
||||
* 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit
|
||||
*/
|
||||
@Column(name = "sms_flag")
|
||||
private Integer smsFlag = 0;
|
||||
|
||||
// ==================== 新版发布表单扩展字段(改造方案 2.1) ====================
|
||||
|
||||
/**
|
||||
* 是否需要学员阅读回执:0 否 / 1 是。
|
||||
* =1 时管理端展示已读/未读统计与「催收未读」入口,学员端展示回执提示;
|
||||
* =0 时阅读仍照常记录(notice_read),但不出催收与回执提示(改动面最小)。
|
||||
*/
|
||||
@Column(name = "receipt_flag")
|
||||
private Integer receiptFlag = 0;
|
||||
|
||||
/**
|
||||
* 审批人快照(JSON 教师ID数组,如 [101,102]):提交审批时由发布人从教师树选定(可多选)。
|
||||
* ApprovalList 据此过滤「待我审批」;或签任一处理后状态即离开待审批,快照留作历史追溯。
|
||||
* 驳回重提并更换审批人后,该快照被新一轮提交覆盖(已知边界,见改造方案 4.4)。
|
||||
*/
|
||||
@Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT")
|
||||
private String approverTeaIds;
|
||||
|
||||
/**
|
||||
* 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。
|
||||
* 立即发布的通知在真实发布时一并置 1。
|
||||
*/
|
||||
@Column(name = "timing_published")
|
||||
private Integer timingPublished = 0;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知审批记录表 notice_approval(改造方案 2.4)。
|
||||
* <p>
|
||||
* 审批轨迹全量落本表(提交行含审批人快照,通过/驳回行记录实际处理人),
|
||||
* 驳回重提产生新一轮提交行,历史轮次永久保留(详情页按时间线分组展示)。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_approval",
|
||||
indexes = @Index(name = "idx_approval_notice", columnList = "notice_id"))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeApproval extends BaseEntity {
|
||||
|
||||
/** 审批动作:1 提交审批 / 2 通过 / 3 驳回(见 ApprovalActionEnum) */
|
||||
public static final int ACTION_SUBMIT = 1;
|
||||
public static final int ACTION_APPROVE = 2;
|
||||
public static final int ACTION_REJECT = 3;
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */
|
||||
@Column(name = "action", nullable = false)
|
||||
private Integer action;
|
||||
|
||||
/** 操作人账号 */
|
||||
@Column(name = "operator_account", length = 64)
|
||||
private String operatorAccount;
|
||||
|
||||
/** 操作人姓名 */
|
||||
@Column(name = "operator_name", length = 64)
|
||||
private String operatorName;
|
||||
|
||||
/** 审批意见 */
|
||||
@Column(name = "opinion", length = 1000)
|
||||
private String opinion;
|
||||
|
||||
/** 仅 action=1 提交行使用:选定审批教师ID快照 JSON [101,102] */
|
||||
@Column(name = "assignee_tea_ids", columnDefinition = "LONGTEXT")
|
||||
private String assigneeTeaIds;
|
||||
|
||||
/** 操作时间 */
|
||||
@Column(name = "action_time")
|
||||
private LocalDateTime actionTime;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yunhai.campusnotice.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;
|
||||
|
||||
/**
|
||||
* 审批人记忆表 notice_approver_memory(改造方案 2.5)。
|
||||
* <p>
|
||||
* 记住发布人最近一次提交所选的审批教师组合,表单再次进入审批流程时
|
||||
* 经 ApproverSuggest 默认回显,免去重复选人。记忆仅是默认值:
|
||||
* Submit 时后端仍按教师ID绑定学校教师校验,防止记忆中的教师离职后带病提交。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_approver_memory",
|
||||
uniqueConstraints = @UniqueConstraint(
|
||||
name = "uk_approver_memory_sch_publisher",
|
||||
columnNames = {"sch_id", "publisher_tea_id"}))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeApproverMemory extends BaseEntity {
|
||||
|
||||
/** 学校ID(多租户隔离) */
|
||||
@Column(name = "sch_id", nullable = false)
|
||||
private Integer schId;
|
||||
|
||||
/** 发布人教师ID(用户中心用户ID,按人记忆) */
|
||||
@Column(name = "publisher_tea_id", nullable = false)
|
||||
private Integer publisherTeaId;
|
||||
|
||||
/** 最近一次所选审批教师ID JSON [101,102] */
|
||||
@Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT")
|
||||
private String approverTeaIds;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 通知推送范围表 notice_push_scope(改造方案 2.2)。
|
||||
* <p>
|
||||
* 范围选择本质是圈定一组学校组织节点,随通知快照保存(防止后续组织调整影响历史通知):
|
||||
* 判定用 scopeId,展示用 scopeName。推送范围不在主表冗余 JSON 字段,统一以本表为准。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_push_scope",
|
||||
indexes = @Index(name = "idx_push_scope_notice", columnList = "notice_id"))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticePushScope extends BaseEntity {
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员(见 PushScopeTypeEnum) */
|
||||
@Column(name = "scope_type", nullable = false)
|
||||
private Integer scopeType;
|
||||
|
||||
/** 对应组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId、全校= schId(仅语义占位) */
|
||||
@Column(name = "scope_id")
|
||||
private Integer scopeId;
|
||||
|
||||
/** 快照名称(如"初二年级"),组织改名不影响历史通知展示 */
|
||||
@Column(name = "scope_name", length = 128)
|
||||
private String scopeName;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义,
|
||||
* 但仅在读者首次查看时落库,避免发通知时预插全员。
|
||||
* <p>
|
||||
* 唯一约束 (notice_id, reader_account):同一用户对同一通知只记一条已读。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_read",
|
||||
uniqueConstraints = @UniqueConstraint(
|
||||
name = "uk_notice_reader",
|
||||
columnNames = {"notice_id", "reader_account"}))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeRead extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 通知ID(关联 notice.id)
|
||||
*/
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/**
|
||||
* 读者登录账号
|
||||
*/
|
||||
@Column(name = "reader_account", nullable = false, length = 64)
|
||||
private String readerAccount;
|
||||
|
||||
/**
|
||||
* 读者姓名
|
||||
*/
|
||||
@Column(name = "reader_name", length = 64)
|
||||
private String readerName;
|
||||
|
||||
/**
|
||||
* 阅读时间
|
||||
*/
|
||||
@Column(name = "read_time")
|
||||
private LocalDateTime readTime;
|
||||
|
||||
// ==================== 阅读身份快照(改造方案 2.3,学员阅读时带出) ====================
|
||||
|
||||
/** 学校ID(取通知 sch_id) */
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/** 年级ID快照 */
|
||||
@Column(name = "grade_id")
|
||||
private Integer gradeId;
|
||||
|
||||
/** 年级名称快照 */
|
||||
@Column(name = "grade_name", length = 128)
|
||||
private String gradeName;
|
||||
|
||||
/** 班级ID快照 */
|
||||
@Column(name = "class_id")
|
||||
private Integer classId;
|
||||
|
||||
/** 班级名称快照 */
|
||||
@Column(name = "class_name", length = 128)
|
||||
private String className;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 通知接收人关联表(快照语义:真实发布时一次写入,后续组织调整不影响历史通知)。
|
||||
* <p>
|
||||
* 教师定向:全员通知(receive_list="ALL")不写明细行,读取时按 receive_list="ALL" 标识走旧逻辑兜底。
|
||||
* 学员圈人(新版发布表单):推送范围按 bschclass 展开为学员账号集合批量写入,
|
||||
* 并携带年级/班级身份快照,供学员端可见性过滤、回执分组(年级→班级→学生)、催收取数。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_receiver",
|
||||
indexes = {
|
||||
@Index(name = "idx_notice_id", columnList = "notice_id"),
|
||||
@Index(name = "idx_account", columnList = "account")
|
||||
})
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeReceiver extends BaseEntity {
|
||||
|
||||
/** 接收类型:1 教师(现状语义,存量数据全为 1) / 2 学员(推送范围解析快照) */
|
||||
public static final int TYPE_TEACHER = 1;
|
||||
public static final int TYPE_STUDENT = 2;
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 接收人登录账号(学员=SchStudent.account) */
|
||||
@Column(name = "account", nullable = false, length = 64)
|
||||
private String account;
|
||||
|
||||
/** 接收类型:1 教师 / 2 学员(原「2 抄送」从未写入过数据,语义重定义,存量无需迁移) */
|
||||
@Column(name = "receive_type", nullable = false)
|
||||
private Integer receiveType = TYPE_TEACHER;
|
||||
|
||||
// ==================== 学员身份快照(发布时从 bschclass 带出,仅 receive_type=2 使用) ====================
|
||||
|
||||
/** 学员姓名快照 */
|
||||
@Column(name = "stu_name", length = 64)
|
||||
private String stuName;
|
||||
|
||||
/** 年级ID快照 */
|
||||
@Column(name = "grade_id")
|
||||
private Integer gradeId;
|
||||
|
||||
/** 年级名称快照(如「初二年级」) */
|
||||
@Column(name = "grade_name", length = 128)
|
||||
private String gradeName;
|
||||
|
||||
/** 班级ID快照(sch_class.class_id) */
|
||||
@Column(name = "class_id")
|
||||
private Integer classId;
|
||||
|
||||
/** 班级名称快照 */
|
||||
@Column(name = "class_name", length = 128)
|
||||
private String className;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 通知模板。对应原 .NET Notify_Template 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_template")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeTemplate extends BaseEntity {
|
||||
|
||||
/** 模板标题 */
|
||||
@Column(name = "title", length = 200)
|
||||
private String title;
|
||||
|
||||
/** 模板内容(HTML) */
|
||||
@Column(name = "content", columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
/** 封面图片文件ID */
|
||||
@Column(name = "cover")
|
||||
private Integer cover;
|
||||
|
||||
/** 引用次数 */
|
||||
@Column(name = "ref_count")
|
||||
private Integer refCount = 0;
|
||||
|
||||
/** 模板类型ID */
|
||||
@Column(name = "type_id")
|
||||
private Integer typeId;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 短信模块配置。对应原 .NET SchSms 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "sms_module")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class SmsModule extends BaseEntity {
|
||||
|
||||
/** 模块名称 */
|
||||
@Column(name = "module_name", length = 100)
|
||||
private String moduleName;
|
||||
|
||||
/** 模块标识 */
|
||||
@Column(name = "module_code", length = 50)
|
||||
private String moduleCode;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 学校短信开通配置。对应原 .NET SchSms_Set 表。
|
||||
* 记录某学校某模块是否开通短信。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "sms_school_config")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class SmsSchoolConfig extends BaseEntity {
|
||||
|
||||
/** 学校ID */
|
||||
@Column(name = "school_id", nullable = false)
|
||||
private Integer schoolId;
|
||||
|
||||
/** 学校名称 */
|
||||
@Column(name = "school_name", length = 200)
|
||||
private String schoolName;
|
||||
|
||||
/** 模块ID */
|
||||
@Column(name = "module_id")
|
||||
private Integer moduleId;
|
||||
|
||||
/** 是否开通短信 */
|
||||
@Column(name = "is_sms")
|
||||
private Boolean isSms = false;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.campusnotice.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,28 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 通知模板类型。对应原 .NET Notify_TemplateType 表。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "template_type")
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class TemplateType extends BaseEntity {
|
||||
|
||||
/** 类型名称 */
|
||||
@Column(name = "type_name", length = 100)
|
||||
private String typeName;
|
||||
|
||||
/** 排序号 */
|
||||
@Column(name = "sort_order")
|
||||
private Integer sortOrder = 0;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yunhai.campusnotice.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,34 @@
|
||||
package com.yunhai.campusnotice.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通知审批动作(notice_approval.action)。
|
||||
*/
|
||||
@Getter
|
||||
public enum ApprovalActionEnum {
|
||||
|
||||
SUBMIT(1, "提交审批"),
|
||||
APPROVE(2, "审批通过"),
|
||||
REJECT(3, "审批驳回");
|
||||
|
||||
private final int code;
|
||||
private final String desc;
|
||||
|
||||
ApprovalActionEnum(int code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static ApprovalActionEnum of(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (ApprovalActionEnum e : values()) {
|
||||
if (e.code == code) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.yunhai.campusnotice.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通知状态机(新版发布表单:直接发布 / 提交审批 / 存为草稿 / 定时发送)。
|
||||
* <pre>
|
||||
* 0 DRAFT 草稿(存为草稿)
|
||||
* 1 PUBLISHED 已发布(直接发布 / 审批通过 / 定时到点)
|
||||
* 2 PENDING 待审批(提交校办审批)
|
||||
* 3 REJECTED 已驳回(可改后重新提交)
|
||||
* </pre>
|
||||
* 兼容性:.NET 原系统「已发布=2」迁移时已归一化为 1,存量数据无 0/2/3;
|
||||
* 实体历史注释「2 定时发送」从未真实写入(定时靠 timingSendTime 字段表达)。
|
||||
* {@link #of(Integer)} 未知值回落 PUBLISHED 的兜底保留。
|
||||
*/
|
||||
@Getter
|
||||
public enum NoticeStatusEnum {
|
||||
|
||||
DRAFT(0, "草稿"),
|
||||
PUBLISHED(1, "已发布"),
|
||||
PENDING(2, "待审批"),
|
||||
REJECTED(3, "已驳回");
|
||||
|
||||
private final int code;
|
||||
private final String desc;
|
||||
|
||||
NoticeStatusEnum(int code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static NoticeStatusEnum of(Integer code) {
|
||||
if (code == null) {
|
||||
return PUBLISHED;
|
||||
}
|
||||
for (NoticeStatusEnum e : values()) {
|
||||
if (e.code == code) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return PUBLISHED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yunhai.campusnotice.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通知类型。对应原 .NET Notify_Message.type(原系统通知类=2,此处归一化)。
|
||||
*/
|
||||
@Getter
|
||||
public enum NoticeTypeEnum {
|
||||
|
||||
NORMAL(1, "普通公告"),
|
||||
IMPORTANT(2, "重要公告");
|
||||
|
||||
private final int code;
|
||||
private final String desc;
|
||||
|
||||
NoticeTypeEnum(int code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static NoticeTypeEnum of(Integer code) {
|
||||
if (code == null) {
|
||||
return NORMAL;
|
||||
}
|
||||
for (NoticeTypeEnum e : values()) {
|
||||
if (e.code == code) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return NORMAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.yunhai.campusnotice.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 推送范围节点类型(notice_push_scope.scope_type)。
|
||||
* scopeId 对应 bschclass 组织节点:学段=学制ID、年级=gradeId、班级=class_id;
|
||||
* 训练队数据源待确认(改造方案 5.1-#3),暂不支持圈人展开。
|
||||
*/
|
||||
@Getter
|
||||
public enum PushScopeTypeEnum {
|
||||
|
||||
SCHOOL(1, "全校"),
|
||||
SUB_SCHOOL(2, "学部"),
|
||||
GRADE(3, "年级"),
|
||||
CLAZZ(4, "班级"),
|
||||
TEAM(5, "训练队"),
|
||||
STUDENT(6, "指定学员");
|
||||
|
||||
private final int code;
|
||||
private final String desc;
|
||||
|
||||
PushScopeTypeEnum(int code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static PushScopeTypeEnum of(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (PushScopeTypeEnum e : values()) {
|
||||
if (e.code == code) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.yunhai.campusnotice.job;
|
||||
|
||||
import com.yunhai.campusnotice.service.INoticeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 定时发送调度任务(改造方案 4.2)。
|
||||
* <p>每分钟扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知,
|
||||
* 逐条 CAS 抢占(timing_published 0→1)后执行真实发布(圈人/落学员快照/发短信/统计口径对齐)。
|
||||
* 幂等由 CAS 保证:重复扫描同一通知只触发一次真实发布。</p>
|
||||
* <p>注意:当前按单实例部署实现(无分布式锁);若后续多实例部署,
|
||||
* 需引入分布式锁(DB 行锁/Redis)或保留 CAS 抢占语义由数据库串行化。</p>
|
||||
*/
|
||||
@Component
|
||||
public class TimingPublishJob {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TimingPublishJob.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Scheduled(fixedDelay = 60_000, initialDelay = 30_000)
|
||||
public void publishDue() {
|
||||
try {
|
||||
int count = noticeService.timingPublishDue();
|
||||
if (count > 0) {
|
||||
log.info("[TimingPublishJob] 本轮定时发布 {} 条通知", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 单条失败不影响下一轮扫描(事务内单条异常会整体回滚该条,由日志暴露)
|
||||
log.error("[TimingPublishJob] 定时发布调度异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.yunhai.campusnotice.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.campusnotice.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.campusnotice.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,29 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchClassDto {
|
||||
|
||||
/** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */
|
||||
@JsonProperty("ClassID")
|
||||
private Integer classId;
|
||||
|
||||
/** 班级名称 */
|
||||
@JsonProperty("ClassName")
|
||||
private String className;
|
||||
|
||||
/** 年级ID(= gradeId) */
|
||||
@JsonProperty("Levell")
|
||||
private Integer gradeId;
|
||||
|
||||
/** 排序号 */
|
||||
@JsonProperty("sx")
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchGradeDto {
|
||||
|
||||
/** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */
|
||||
@JsonProperty("Id")
|
||||
private Integer id;
|
||||
|
||||
/** 年级名称(gradeNickname,如「初一」) */
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
/** 所属学段(学制)ID */
|
||||
@JsonProperty("OwnerId")
|
||||
private Integer ownerId;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchInfoDto {
|
||||
|
||||
/** 学校 GUID(bschclass 侧 SchSerId) */
|
||||
@JsonProperty("SchSerId")
|
||||
private String schSerId;
|
||||
|
||||
/** 用户中心学校组ID(与 CampusNotice schId 同域) */
|
||||
@JsonProperty("UserSchSerId")
|
||||
private Integer userSchSerId;
|
||||
|
||||
/** 学校名称 */
|
||||
@JsonProperty("SchName")
|
||||
private String schName;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。
|
||||
* 登录账号口径:学员端账号 = SchStudent.account(改造方案 5.1-#1)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchStudentDto {
|
||||
|
||||
/** 学员登录账号 */
|
||||
@JsonProperty("Account")
|
||||
private String account;
|
||||
|
||||
/** 学生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,25 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchSystemDto {
|
||||
|
||||
/** 学制ID(scopeType=学部 的 scopeId) */
|
||||
@JsonProperty("Id")
|
||||
private Integer id;
|
||||
|
||||
/** 学段名称(如「小学部」「初中部」) */
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
/** 学制类型 */
|
||||
@JsonProperty("Type")
|
||||
private Integer type;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.yunhai.campusnotice.model.bsch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。
|
||||
* type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SchTreeNodeDto {
|
||||
|
||||
/** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */
|
||||
private Integer id;
|
||||
|
||||
/** 节点名称 */
|
||||
private String name;
|
||||
|
||||
/** 父节点ID(根节点为 0) */
|
||||
private Integer pid;
|
||||
|
||||
/** 排序号(组取 sort,叶子固定 0) */
|
||||
private Integer no;
|
||||
|
||||
/** 1=组节点,2=人员叶子 */
|
||||
private Integer type;
|
||||
|
||||
/** 是否父节点(组 true,叶子 false) */
|
||||
@JsonProperty("isParent")
|
||||
private Boolean isParent;
|
||||
|
||||
/** 头像(当前固定空串) */
|
||||
private String photo;
|
||||
|
||||
/** 子节点(叶子为 null,组节点非 null) */
|
||||
private List<SchTreeNodeDto> children;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批列表行 VO(notice/ApprovalList,待审/已审合一)。
|
||||
* <p>已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false),
|
||||
* 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalListVo {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String title;
|
||||
|
||||
/** 通知类型:1 普通 / 2 重要 */
|
||||
private Integer type;
|
||||
|
||||
private String typeDesc;
|
||||
|
||||
/** 状态:2 待审批 / 1 已发布 / 3 已驳回 */
|
||||
private Integer status;
|
||||
|
||||
private String statusDesc;
|
||||
|
||||
/** 发布人账号 */
|
||||
private String publisherAccount;
|
||||
|
||||
/** 发布人姓名 */
|
||||
private String publisherName;
|
||||
|
||||
/** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */
|
||||
private List<String> approverNames = new ArrayList<>();
|
||||
|
||||
/** 最新一轮提交时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime submitTime;
|
||||
|
||||
/** 是否需要学员阅读回执 */
|
||||
private Integer receiptFlag;
|
||||
|
||||
/** 定时发送时间(审批通过后到点真实发布) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
// ==================== 已审 Tab 字段(待审 Tab 为空) ====================
|
||||
|
||||
/** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */
|
||||
private boolean handled;
|
||||
|
||||
/** 是否由当前登录人处理(false=或签下他人已处理) */
|
||||
private boolean handledByMe;
|
||||
|
||||
/** 实际处理人账号 */
|
||||
private String handledAccount;
|
||||
|
||||
/** 实际处理人姓名 */
|
||||
private String handledName;
|
||||
|
||||
/** 实际处理动作:2 通过 / 3 驳回 */
|
||||
private Integer handledAction;
|
||||
|
||||
/** 实际处理动作描述 */
|
||||
private String handledActionDesc;
|
||||
|
||||
/** 实际处理时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime handledTime;
|
||||
|
||||
/** 处理意见(长文本建议进详情查看) */
|
||||
private String handledOpinion;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批轨迹记录 VO(详情页全量时间线,按轮次分组由前端按提交行切分)。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalRecordVo {
|
||||
|
||||
/** 审批动作:1 提交审批 / 2 通过 / 3 驳回 */
|
||||
private Integer action;
|
||||
|
||||
/** 动作描述 */
|
||||
private String actionDesc;
|
||||
|
||||
/** 操作人账号 */
|
||||
private String operatorAccount;
|
||||
|
||||
/** 操作人姓名 */
|
||||
private String operatorName;
|
||||
|
||||
/** 审批意见(提交行=提交说明) */
|
||||
private String opinion;
|
||||
|
||||
/** 仅提交行:该轮审批教师ID快照(教师树叶子节点ID) */
|
||||
private List<Integer> assigneeTeaIds = new ArrayList<>();
|
||||
|
||||
/** 操作时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime actionTime;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 审批人解析项:教师树节点ID(用户中心用户ID)与登录账号互转。
|
||||
* <p>教师树(base/GetSchoolAll)叶子仅携带教师用户ID与姓名,审批快照按登录账号存储;
|
||||
* 前端选人后经 ApproverResolve 换算账号提交,编辑回显时按账号换回节点ID勾树。
|
||||
* 按账号解析失败时 id 为 null,按ID解析失败时 account 为 null(调用方据此提示)。</p>
|
||||
*/
|
||||
@Data
|
||||
public class ApproverResolveVo {
|
||||
|
||||
/** 教师用户ID(教师树叶子节点 id) */
|
||||
private Integer id;
|
||||
|
||||
/** 头像 */
|
||||
private String photo;
|
||||
|
||||
/** 姓名 */
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批人记忆回显 VO(notice/ApproverSuggest)。
|
||||
* 无记忆时返回空列表,前端走通用选人;姓名由前端经 ApproverResolve 按学校教师绑定。
|
||||
*/
|
||||
@Data
|
||||
public class ApproverSuggestVo {
|
||||
|
||||
/** 审批教师ID列表(教师树叶子节点ID) */
|
||||
private List<Integer> teaIds = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.campusnotice.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,112 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知详情视图对象(替代原 Map<String,Object> 返回)。
|
||||
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeDetailVo {
|
||||
|
||||
@JsonProperty("serid")
|
||||
private Integer serid;
|
||||
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@JsonProperty("Contents")
|
||||
private String contents;
|
||||
|
||||
@JsonProperty("DeptID")
|
||||
private String deptId;
|
||||
|
||||
@JsonProperty("DeptName")
|
||||
private String deptName;
|
||||
|
||||
@JsonProperty("TeaName")
|
||||
private String publisherName;
|
||||
|
||||
@JsonProperty("EditDate")
|
||||
private String editDate;
|
||||
|
||||
@JsonProperty("numcount")
|
||||
private Integer numCount;
|
||||
|
||||
@JsonProperty("readnumcount")
|
||||
private Long readNumCount;
|
||||
|
||||
@JsonProperty("ReceiveList")
|
||||
private String receiveList;
|
||||
|
||||
@JsonProperty("DsfsTime")
|
||||
private String dsfsTime;
|
||||
|
||||
@JsonProperty("isDsfs")
|
||||
private Boolean isDsfs;
|
||||
|
||||
@JsonProperty("ChaoSend")
|
||||
private List<FileItem> chaoSend;
|
||||
|
||||
/** 接收人列表:"all" 或账号列表 */
|
||||
@JsonProperty("tlist")
|
||||
private Object tlist;
|
||||
|
||||
// ==================== 新版发布表单扩展(改造方案 3.2 / 4.6) ====================
|
||||
|
||||
/** 重要程度:1 普通 / 2 重要(编辑回显) */
|
||||
@JsonProperty("type")
|
||||
private Integer type;
|
||||
|
||||
/** 是否置顶:0 否 / 1 是(编辑回显) */
|
||||
@JsonProperty("topmost")
|
||||
private Integer topmost;
|
||||
|
||||
/** 短信提醒标记:0 否 / 1 是(编辑回显) */
|
||||
@JsonProperty("smsFlag")
|
||||
private Integer smsFlag;
|
||||
|
||||
/** 状态:0 草稿 / 1 已发布 / 2 待审批 / 3 已驳回 */
|
||||
@JsonProperty("status")
|
||||
private Integer status;
|
||||
|
||||
/** 状态描述 */
|
||||
@JsonProperty("statusDesc")
|
||||
private String statusDesc;
|
||||
|
||||
/** 是否需要学员阅读回执:0 否 / 1 是 */
|
||||
@JsonProperty("receiptFlag")
|
||||
private Integer receiptFlag;
|
||||
|
||||
/** 推送范围快照(编辑回显) */
|
||||
@JsonProperty("pushScopes")
|
||||
private List<PushScopeItem> pushScopes;
|
||||
|
||||
/** 审批轨迹(管理端详情展示;学员端 mobiapi 不返回) */
|
||||
@JsonProperty("approvalRecords")
|
||||
private List<ApprovalRecordVo> approvalRecords;
|
||||
|
||||
/** 当前绑定的审批教师ID快照(待审批/已驳回通知的编辑回显,按ID勾选教师树;姓名由前端经 ApproverResolve 绑定) */
|
||||
@JsonProperty("approverTeaIds")
|
||||
private List<Integer> approverTeaIds;
|
||||
|
||||
/**
|
||||
* 附件项(旧 ChaoSend 数组元素)。
|
||||
*/
|
||||
@Data
|
||||
public static class FileItem {
|
||||
@JsonProperty("id")
|
||||
private Integer id;
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
@JsonProperty("size")
|
||||
private Integer size;
|
||||
@JsonProperty("fileid")
|
||||
private Integer fileid;
|
||||
@JsonProperty("filename")
|
||||
private String filename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.yunhai.campusnotice.model.BasePageResult;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。
|
||||
* <p>
|
||||
* 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class NoticePageResult<T> extends BasePageResult<T> {
|
||||
|
||||
/** 当前用户未读通知总数 */
|
||||
private long unreadCount;
|
||||
|
||||
public NoticePageResult(List<T> content, int total, int pageIndex, int pageSize,
|
||||
int totalPages, boolean hasNext, long unreadCount) {
|
||||
super(content, total, pageIndex, pageSize, totalPages, hasNext);
|
||||
this.unreadCount = unreadCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.yunhai.campusnotice.model.BasePageQuery;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 通知列表查询条件。
|
||||
* <p>
|
||||
* - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤;
|
||||
* - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class NoticeQuery extends BasePageQuery {
|
||||
|
||||
/** 状态过滤:null 不过滤;1 已发布 */
|
||||
private Integer status;
|
||||
|
||||
/** 类型过滤:1 普通 / 2 重要 */
|
||||
private Integer type;
|
||||
|
||||
/** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */
|
||||
private String scope;
|
||||
|
||||
/** 发布部门ID */
|
||||
private String deptId;
|
||||
|
||||
/** 发布人账号(管理端按发布人查) */
|
||||
private String publisherAccount;
|
||||
|
||||
/** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate startTime;
|
||||
|
||||
/** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate endTime;
|
||||
|
||||
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||
private Integer readType = 0;
|
||||
|
||||
/** 排序:1 时间倒序(默认)/ 2 时间正序 */
|
||||
private String orderByTime = "1";
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import com.yunhai.campusnotice.common.validation.ValidationGroups;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeRequest {
|
||||
|
||||
/** 通知标题 */
|
||||
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空")
|
||||
private String title;
|
||||
|
||||
/** 通知内容 */
|
||||
private String content;
|
||||
|
||||
/** 通知类型:1 普通 / 2 重要 */
|
||||
@NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空")
|
||||
private Integer type = 1;
|
||||
|
||||
/** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */
|
||||
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空")
|
||||
private String deptId;
|
||||
|
||||
/** 发布部门名称 */
|
||||
private String deptName;
|
||||
|
||||
/** 是否置顶:0 否 / 1 是 */
|
||||
private Integer topmost = 0;
|
||||
|
||||
/** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */
|
||||
private String receiveList;
|
||||
|
||||
/** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */
|
||||
private String fileIds;
|
||||
|
||||
/** 定时发送时间(为空表示立即发布) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */
|
||||
private Integer smsFlag = 0;
|
||||
|
||||
/** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */
|
||||
private Integer sourceId;
|
||||
|
||||
// ==================== 新版发布表单扩展(改造方案 3.2) ====================
|
||||
|
||||
/** 推送范围列表(与 receiveList 可并存:pushScopes 解析学员,receiveList 作为教师定向名单) */
|
||||
private List<PushScopeItem> pushScopes;
|
||||
|
||||
/** 是否需要学员阅读回执:0 否 / 1 是 */
|
||||
private Integer receiptFlag = 0;
|
||||
|
||||
/** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */
|
||||
private String publishType;
|
||||
|
||||
/** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */
|
||||
private List<Integer> approverTeaIds;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知统计视图(对应原系统 GetStatisData / 按发布人统计)。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeStatisticsVo {
|
||||
|
||||
/** 通知总数(全部已发布) */
|
||||
private long total;
|
||||
|
||||
/** 已发布数 */
|
||||
private long publishedCount;
|
||||
|
||||
/** 阅读记录总数(累计已读人次) */
|
||||
private long totalReadRecords;
|
||||
|
||||
/** 当前用户未读数(已到点的已发布 - 当前用户已读) */
|
||||
private long unreadForCurrentUser;
|
||||
|
||||
/** 按发布人统计 */
|
||||
private List<PublisherStat> byPublisher;
|
||||
|
||||
/** 按月统计(已发布通知数) */
|
||||
private List<MonthStat> byMonth;
|
||||
|
||||
@Data
|
||||
public static class PublisherStat {
|
||||
private String publisherName;
|
||||
private long count;
|
||||
|
||||
public PublisherStat(String publisherName, long count) {
|
||||
this.publisherName = publisherName;
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MonthStat {
|
||||
private int year;
|
||||
private int month;
|
||||
private long count;
|
||||
|
||||
public MonthStat(int year, int month, long count) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户端通知列表结果:在分页数据之外额外携带「当前用户未读总数」,
|
||||
* 供前端展示未读角标(对应原系统 OtherData.unReadCount)。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeUserListResult {
|
||||
|
||||
private int total;
|
||||
private List<NoticeVo> content;
|
||||
private int pageIndex;
|
||||
private int pageSize;
|
||||
private int totalPages;
|
||||
private boolean hasNext;
|
||||
|
||||
/** 当前用户未读通知总数 */
|
||||
private long unreadCount;
|
||||
|
||||
public NoticeUserListResult(List<NoticeVo> content, int total, int pageIndex, int pageSize,
|
||||
int totalPages, boolean hasNext, long unreadCount) {
|
||||
this.content = content;
|
||||
this.total = total;
|
||||
this.pageIndex = pageIndex;
|
||||
this.pageSize = pageSize;
|
||||
this.totalPages = totalPages;
|
||||
this.hasNext = hasNext;
|
||||
this.unreadCount = unreadCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知视图对象(列表/详情返回)。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeVo {
|
||||
|
||||
private Integer id;
|
||||
private String title;
|
||||
private String content;
|
||||
private Integer type;
|
||||
private String typeDesc;
|
||||
private String publisherAccount;
|
||||
private String publisherName;
|
||||
private String deptId;
|
||||
private String deptName;
|
||||
private Integer status;
|
||||
private String statusDesc;
|
||||
private Integer topmost;
|
||||
|
||||
/** 已读人数(基于阅读记录实时统计) */
|
||||
private Long readCount = 0L;
|
||||
|
||||
/** 应读人数 */
|
||||
private Integer allowCount;
|
||||
|
||||
/** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */
|
||||
private boolean read = false;
|
||||
|
||||
/** 是否撤回:0 否 / 1 是 */
|
||||
private Integer isPulled;
|
||||
|
||||
/** 定时发送时间 */
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 父通知ID(转发时关联原通知) */
|
||||
private Integer parentId;
|
||||
|
||||
/** 短信发送标记 */
|
||||
private Integer smsFlag;
|
||||
|
||||
/** 接收人列表(JSON) */
|
||||
private String receiveList;
|
||||
|
||||
// ==================== 新版发布表单扩展(改造方案 3.2) ====================
|
||||
|
||||
/** 是否需要学员阅读回执:0 否 / 1 是 */
|
||||
private Integer receiptFlag;
|
||||
|
||||
/** 定时发送已真正发布标记:0 未触发 / 1 已真实发布(配合 timingSendTime 区分「定时中/已发布」) */
|
||||
private Integer timingPublished;
|
||||
|
||||
/** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */
|
||||
private String approverTeaIds;
|
||||
|
||||
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
|
||||
private String UpFile;
|
||||
|
||||
private String createBy;
|
||||
private LocalDateTime createTime;
|
||||
private String updateBy;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 阅读明细视图(阅读列表查询用)。
|
||||
*/
|
||||
@Data
|
||||
public static class ReadDetail {
|
||||
private String account;
|
||||
private String name;
|
||||
private boolean read;
|
||||
private LocalDateTime readTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 推送范围项(发布/编辑入参与详情回显)。
|
||||
* scopeType 见 {@link com.yunhai.campusnotice.enums.PushScopeTypeEnum}。
|
||||
*/
|
||||
@Data
|
||||
public class PushScopeItem {
|
||||
|
||||
/** 范围类型:1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员 */
|
||||
private Integer scopeType;
|
||||
|
||||
/** 组织节点ID:学部=学制ID、年级=gradeId、班级=class_id、学员=stuId */
|
||||
private Integer scopeId;
|
||||
|
||||
/** 节点名称快照 */
|
||||
private String scopeName;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 已发送通知阅读回执 VO(notice/ReadReceipt)。
|
||||
* 按身份快照分组:年级 → 班级 → 学生(前端已读/未读着色),教师接收行归「教师」组。
|
||||
* 数据源=notice_receiver 学员快照 + notice_read 懒标记,不实时调用 bschclass。
|
||||
*/
|
||||
@Data
|
||||
public class ReadReceiptVo {
|
||||
|
||||
/** 通知ID */
|
||||
private Integer serid;
|
||||
|
||||
/** 应读人数(notice.allow_count) */
|
||||
private Integer allowCount;
|
||||
|
||||
/** 已读人数(notice_read 实时统计) */
|
||||
private Long readCount;
|
||||
|
||||
/** 年级分组 */
|
||||
private List<GradeGroup> grades = new ArrayList<>();
|
||||
|
||||
/** 教师接收行(无年级/班级维度) */
|
||||
private List<ReaderItem> teachers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 年级分组。
|
||||
*/
|
||||
@Data
|
||||
public static class GradeGroup {
|
||||
private Integer gradeId;
|
||||
private String gradeName;
|
||||
private List<ClassGroup> classes = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 班级分组。
|
||||
*/
|
||||
@Data
|
||||
public static class ClassGroup {
|
||||
private Integer classId;
|
||||
private String className;
|
||||
private List<ReaderItem> students = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 学生/教师阅读项。
|
||||
*/
|
||||
@Data
|
||||
public static class ReaderItem {
|
||||
private String account;
|
||||
private String name;
|
||||
/** 是否已读 */
|
||||
private boolean read;
|
||||
/** 阅读时间(未读为 null) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime readTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 推送范围预估 VO(notice/ScopePreview)。
|
||||
*/
|
||||
@Data
|
||||
public class ScopePreviewVo {
|
||||
|
||||
/** 范围展开学员数(按账号去重) */
|
||||
private int studentCount;
|
||||
|
||||
/** 教师定向名单数(receiveList 解析) */
|
||||
private int teacherCount;
|
||||
|
||||
/** 合计预估接收人数 */
|
||||
private int totalCount;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提交审批入参(notice/Submit)。
|
||||
* 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。
|
||||
*/
|
||||
@Data
|
||||
public class SubmitRequest {
|
||||
|
||||
/** 通知ID */
|
||||
private Integer serid;
|
||||
|
||||
/** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */
|
||||
private List<Integer> approverTeaIds;
|
||||
|
||||
/** 提交说明(可选) */
|
||||
private String opinion;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yunhai.campusnotice.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.campusnotice.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.campusnotice.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.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.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);
|
||||
|
||||
/**
|
||||
* 按创建人账号查询联系组列表。
|
||||
*/
|
||||
List<ContactsGroup> findByCreatorAccount(String creatorAccount);
|
||||
|
||||
/**
|
||||
* 按创建人账号查询联系组列表(分页)。
|
||||
*/
|
||||
Page<ContactsGroup> findByCreatorAccount(String creatorAccount, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查询全部联系组(按创建时间降序)。
|
||||
*/
|
||||
List<ContactsGroup> findAllByOrderByCreateTimeDesc();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.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,32 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeApproval;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知审批记录仓储。
|
||||
*/
|
||||
public interface NoticeApprovalRepository extends JpaRepository<NoticeApproval, Integer> {
|
||||
|
||||
/** 某通知全量审批轨迹(按落库顺序,即轮次时间线) */
|
||||
@Query("select a from NoticeApproval a where a.noticeId = :noticeId order by a.id asc")
|
||||
List<NoticeApproval> findByNoticeIdOrderByIdAsc(@Param("noticeId") Integer noticeId);
|
||||
|
||||
/** 批量查 N 个通知的审批记录(审批列表/详情批量装配) */
|
||||
@Query("select a from NoticeApproval a where a.noticeId in :noticeIds order by a.id asc")
|
||||
List<NoticeApproval> findByNoticeIds(@Param("noticeIds") Collection<Integer> noticeIds);
|
||||
|
||||
/** 某通知最新一条指定动作的记录(或签「他人已处理」摘要 / 驳回意见回显) */
|
||||
@Query("select a from NoticeApproval a where a.noticeId = :noticeId and a.action in :actions " +
|
||||
"order by a.id desc")
|
||||
List<NoticeApproval> findLatestByNoticeIdAndActions(@Param("noticeId") Integer noticeId,
|
||||
@Param("actions") Collection<Integer> actions);
|
||||
|
||||
/** 按通知ID删除全部审批记录(通知删除时级联清理) */
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeApproverMemory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 审批人记忆仓储(唯一约束 sch_id + publisher_tea_id,upsert 由 Service 完成)。
|
||||
*/
|
||||
public interface NoticeApproverMemoryRepository extends JpaRepository<NoticeApproverMemory, Integer> {
|
||||
|
||||
/** 按学校 + 发布人教师ID查最近一次所选审批教师组合 */
|
||||
Optional<NoticeApproverMemory> findBySchIdAndPublisherTeaId(Integer schId, Integer publisherTeaId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticePushScope;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知推送范围仓储。
|
||||
*/
|
||||
public interface NoticePushScopeRepository extends JpaRepository<NoticePushScope, Integer> {
|
||||
|
||||
/** 按通知ID查询全部范围快照 */
|
||||
List<NoticePushScope> findByNoticeId(Integer noticeId);
|
||||
|
||||
/** 批量查 N 个通知的范围(编辑回显/列表展示避免 N+1) */
|
||||
@Query("select s from NoticePushScope s where s.noticeId in :noticeIds")
|
||||
List<NoticePushScope> findByNoticeIds(@Param("noticeIds") List<Integer> noticeIds);
|
||||
|
||||
/** 通知是否存在推送范围(学员端可见性过滤仅对范围通知启用) */
|
||||
boolean existsByNoticeId(Integer noticeId);
|
||||
|
||||
/** 按通知ID删除全部范围(编辑全量替换) */
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeRead;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 通知阅读记录仓储(懒标记模型)。
|
||||
*/
|
||||
public interface NoticeReadRepository extends JpaRepository<NoticeRead, Integer> {
|
||||
|
||||
/**
|
||||
* 某用户是否已读某通知。
|
||||
*/
|
||||
boolean existsByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount);
|
||||
|
||||
/**
|
||||
* 某通知的总已读人数。
|
||||
*/
|
||||
long countByNoticeId(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 删除某通知的全部阅读记录(通知删除时级联清理)。
|
||||
*/
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 某用户已读的通知总数(用于「我的未读」= 已发布总数 - 已读总数)。
|
||||
*/
|
||||
long countByReaderAccount(String readerAccount);
|
||||
|
||||
/**
|
||||
* 查询某用户已读的通知ID列表(用于列表页批量判定未读)。
|
||||
*/
|
||||
@Query("select r.noticeId from NoticeRead r where r.readerAccount = :account")
|
||||
List<Integer> findNoticeIdsByReaderAccount(@Param("account") String account);
|
||||
|
||||
/**
|
||||
* 查询某用户对某通知的阅读记录(标记已读时幂等判断)。
|
||||
*/
|
||||
Optional<NoticeRead> findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount);
|
||||
|
||||
/**
|
||||
* 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。
|
||||
*/
|
||||
@Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc")
|
||||
List<NoticeRead> findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeReceiver;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 通知接收人关联表仓储。
|
||||
*/
|
||||
public interface NoticeReceiverRepository extends JpaRepository<NoticeReceiver, Integer> {
|
||||
|
||||
/** 按通知ID查询全部接收人 */
|
||||
List<NoticeReceiver> findByNoticeId(Integer noticeId);
|
||||
|
||||
/** 按通知ID删除全部接收人 */
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
|
||||
/** 按通知ID和接收类型查询接收人 */
|
||||
List<NoticeReceiver> findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType);
|
||||
|
||||
/**
|
||||
* 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。
|
||||
* @param noticeIds 通知ID列表
|
||||
* @return 接收人关联记录列表
|
||||
*/
|
||||
@Query("select r from NoticeReceiver r where r.noticeId in :noticeIds")
|
||||
List<NoticeReceiver> findByNoticeIds(@Param("noticeIds") List<Integer> noticeIds);
|
||||
|
||||
// ==================== 新版发布流程(改造方案 2.3 / 3.2) ====================
|
||||
|
||||
/** 当前账号命中的通知ID集合(学员端列表可见性过滤:仅对存在范围的通知启用) */
|
||||
@Query("select distinct r.noticeId from NoticeReceiver r where r.account = :account")
|
||||
List<Integer> findNoticeIdsByAccount(@Param("account") String account);
|
||||
|
||||
/** 某人某通知的接收快照行(markRead 身份快照取数:学员行优先) */
|
||||
Optional<NoticeReceiver> findFirstByNoticeIdAndAccountOrderByReceiveTypeDescIdDesc(
|
||||
Integer noticeId, String account);
|
||||
|
||||
/** 按通知ID + 接收类型删除(学员圈人重新展开时清理旧快照) */
|
||||
void deleteByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType);
|
||||
|
||||
/** 按通知ID + 接收类型计数(allowCount = 学员数 + 教师数) */
|
||||
long countByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.Notice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知公告仓储。
|
||||
*/
|
||||
public interface NoticeRepository extends JpaRepository<Notice, Integer>,
|
||||
JpaSpecificationExecutor<Notice> {
|
||||
|
||||
/**
|
||||
* 按状态计数(用于统计已发布总数)。
|
||||
*/
|
||||
long countByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。
|
||||
*/
|
||||
@Query("select count(n) from Notice n where n.status = :status " +
|
||||
"and (n.timingSendTime is null or n.timingSendTime <= :now)")
|
||||
long countPublishedDue(@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。
|
||||
*/
|
||||
@Query("select n.createTime from Notice n " +
|
||||
"where n.status = :status and n.createTime >= :start and n.createTime <= :end")
|
||||
List<LocalDateTime> findCreateTimesByStatusAndRange(@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 查询指定状态、指定时间范围内的发布人姓名(用于按发布人统计)。
|
||||
*/
|
||||
@Query("select n.publisherName from Notice n " +
|
||||
"where n.status = :status and n.createTime >= :start and n.createTime <= :end")
|
||||
List<String> findPublisherNamesByStatusAndRange(@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 按学校和状态计数(多租户隔离统计)。
|
||||
*/
|
||||
long countBySchIdAndStatus(Integer schId, Integer status);
|
||||
|
||||
/**
|
||||
* 按学校计数(多租户隔离统计:全部状态)。
|
||||
*/
|
||||
long countBySchId(Integer schId);
|
||||
|
||||
/**
|
||||
* 排除指定状态计数(统计"累计发布"时剔除草稿)。
|
||||
*/
|
||||
long countByStatusNot(Integer status);
|
||||
|
||||
/**
|
||||
* 按学校排除指定状态计数(多租户隔离统计,剔除草稿)。
|
||||
*/
|
||||
long countBySchIdAndStatusNot(Integer schId, Integer status);
|
||||
|
||||
/**
|
||||
* 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " +
|
||||
"and (n.timingSendTime is null or n.timingSendTime <= :now)")
|
||||
long countPublishedDueBySchId(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select n.createTime from Notice n " +
|
||||
"where n.schId = :schId and n.status = :status " +
|
||||
"and n.createTime >= :start and n.createTime <= :end")
|
||||
List<LocalDateTime> findCreateTimesBySchIdStatusAndRange(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 查询指定学校、指定状态、指定时间范围内的发布人姓名(按发布人统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select n.publisherName from Notice n " +
|
||||
"where n.schId = :schId and n.status = :status " +
|
||||
"and n.createTime >= :start and n.createTime <= :end")
|
||||
List<String> findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
// ==================== 新版发布流程(改造方案 4.2 / 4.3) ====================
|
||||
|
||||
/**
|
||||
* 定时发送待触发列表:已发布、已到点、尚未真正发布(timing_published=0)。
|
||||
* TimingPublishJob 每分钟扫描,逐条 CAS 抢占后执行真实发布。
|
||||
*/
|
||||
@Query("select n from Notice n where n.status = 1 " +
|
||||
"and n.timingSendTime is not null and n.timingSendTime <= :now " +
|
||||
"and n.timingPublished = 0")
|
||||
List<Notice> findDueTimingPublish(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 定时发布幂等抢占:仅当 timing_published 仍为 0 时置 1,返回影响行数(0=已被其他线程/实例处理)。
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update Notice n set n.timingPublished = 1, n.updateTime = :now " +
|
||||
"where n.id = :id and n.timingPublished = 0")
|
||||
int casMarkTimingPublished(@Param("id") Integer id, @Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 审批通过 CAS:仅当仍处于待审批(status=2)时置为已发布,返回影响行数。
|
||||
* 或签并发场景下先提交者生效,后提交者影响行数为 0,由 Service 转为冲突提示。
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update Notice n set n.status = 1, n.updateTime = :now " +
|
||||
"where n.id = :id and n.status = 2")
|
||||
int casApprove(@Param("id") Integer id, @Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 审批驳回 CAS:仅当仍处于待审批(status=2)时置为已驳回。
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update Notice n set n.status = 3, n.updateTime = :now " +
|
||||
"where n.id = :id and n.status = 2")
|
||||
int casReject(@Param("id") Integer id, @Param("now") LocalDateTime now);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeTemplate;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface NoticeTemplateRepository extends JpaRepository<NoticeTemplate, Integer> {
|
||||
Page<NoticeTemplate> findByTypeId(Integer typeId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查询全部模板(按引用次数降序),分页。
|
||||
*/
|
||||
Page<NoticeTemplate> findAllByOrderByRefCountDesc(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。
|
||||
*/
|
||||
long countByTypeId(Integer typeId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.SmsModule;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SmsModuleRepository extends JpaRepository<SmsModule, Integer> {
|
||||
List<SmsModule> findAllByOrderByIdAsc();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.SmsSchoolConfig;
|
||||
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 SmsSchoolConfigRepository extends JpaRepository<SmsSchoolConfig, Integer> {
|
||||
Page<SmsSchoolConfig> findBySchoolNameContaining(String schoolName, Pageable pageable);
|
||||
Page<SmsSchoolConfig> findAll(Pageable pageable);
|
||||
List<SmsSchoolConfig> findBySchoolId(Integer schoolId);
|
||||
List<SmsSchoolConfig> findBySchoolIdInAndModuleIdIn(List<Integer> schoolIds, List<Integer> moduleIds);
|
||||
|
||||
/**
|
||||
* 按学校ID和模块ID查询单条配置(判断是否已存在)。
|
||||
*/
|
||||
SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId);
|
||||
|
||||
/**
|
||||
* 按学校ID查询全部模块配置(用于"设置某校全部模块")。
|
||||
*/
|
||||
List<SmsSchoolConfig> findBySchoolIdIn(List<Integer> schoolIds);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.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,20 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.TemplateType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TemplateTypeRepository extends JpaRepository<TemplateType, Integer> {
|
||||
List<TemplateType> findAllByOrderBySortOrderAsc();
|
||||
|
||||
/**
|
||||
* 按类型名称查询(重名校验)。
|
||||
*/
|
||||
boolean existsByTypeName(String typeName);
|
||||
|
||||
/**
|
||||
* 按类型名称查询(重名校验,排除自身)。
|
||||
*/
|
||||
boolean existsByTypeNameAndIdNot(String typeName, Integer id);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.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,88 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import com.yunhai.campusnotice.model.SchTeaList;
|
||||
import com.yunhai.campusnotice.model.bsch.SchClassDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchGradeDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchInfoDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchStudentDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchSystemDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理(bschclass)开放接口客户端门面。
|
||||
* <p>基址取配置 {@code yh.open.bschclass}(/oapi/** 无需应用签名,独立 RestTemplate 直连)。
|
||||
* 推送范围树、圈人、身份快照统一经本门面取数,不散落 RestTemplate 调用(改造方案 2.0)。
|
||||
*/
|
||||
public interface IClassService {
|
||||
|
||||
/**
|
||||
* 获取指定学校全部教师(含所属组 GroupIds)。
|
||||
* 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}}
|
||||
*
|
||||
* @param schId 学校ID(即用户当前身份的 schId)
|
||||
*/
|
||||
List<SchTeaList> getSchTeaList(int schId);
|
||||
|
||||
/**
|
||||
* 用户中心学校ID → bschclass 学校信息(SchSerId / 学校名)。
|
||||
* 对应:GET {@code {bschclass}/oapi/SchInfo/GetUserSchFirst?UserSchID={schId}}
|
||||
*/
|
||||
SchInfoDto getSchFirst(int schId);
|
||||
|
||||
/**
|
||||
* 学校学段(学制)列表:小学部 / 初中部…。
|
||||
* 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenlist?SchSerID=}
|
||||
*/
|
||||
List<SchSystemDto> getSchLenList(String schSerId);
|
||||
|
||||
/**
|
||||
* 学校年级列表(Id=gradeId、Name=gradeNickname、OwnerId=学段id)。
|
||||
* 对应:GET {@code {bschclass}/oapi/Schoolen/GetSchLenLevellist?SchSerID=}
|
||||
*/
|
||||
List<SchGradeDto> getSchGradeList(String schSerId);
|
||||
|
||||
/**
|
||||
* 学校全部在读班级(ClassID/ClassName/Levell=gradeId),ScopeTree 一次取数客户端组装。
|
||||
* 对应:GET {@code {bschclass}/oapi/VSchClass/GetVClassList?SchSerID=}
|
||||
*/
|
||||
List<SchClassDto> getSchClassList(String schSerId);
|
||||
|
||||
/**
|
||||
* 按校全部学生(圈人:全校范围展开)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetSchStulist?schserid=}
|
||||
*/
|
||||
List<SchStudentDto> getSchStuList(String schSerId);
|
||||
|
||||
/**
|
||||
* 按年级直查学生(圈人:年级范围免逐班聚合;bschclass 侧补缺接口)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetGradeStulist?SchSerID=&GradeId=}
|
||||
*/
|
||||
List<SchStudentDto> getGradeStuList(String schSerId, Integer gradeId);
|
||||
|
||||
/**
|
||||
* 按学段直查学生(圈人:学部范围一键展开;bschclass 侧补缺接口)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetSubStulist?SchSerID=&SchSystemId=}
|
||||
*/
|
||||
List<SchStudentDto> getSubStuList(String schSerId, Integer schSystemId);
|
||||
|
||||
/**
|
||||
* 按班级批量查学生(圈人:班级范围一次多班)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetClaArrStulist?ClassIDs=1,2,3}
|
||||
*/
|
||||
List<SchStudentDto> getClaArrStuList(List<Integer> classIds);
|
||||
|
||||
/**
|
||||
* 按学生ID批量查(圈人:指定学员范围)。
|
||||
* 对应:GET {@code {bschclass}/oapi/OAStu/GetStuIdslist?stuids=1,2,3}
|
||||
*/
|
||||
List<SchStudentDto> getStuIdsList(List<Integer> stuIds);
|
||||
|
||||
/**
|
||||
* 学校学生树:学校→学段→年级→班级→学生(单元素列表,根为学校节点)。
|
||||
* 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。
|
||||
* 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=}
|
||||
*/
|
||||
List<SchTreeNodeDto> getStuTree(Integer schId);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.campusnotice.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,74 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典业务接口。
|
||||
* 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。
|
||||
*/
|
||||
public interface IDictService {
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
* C# GetSysDic: Type=1 部门类别, 其余类型略。
|
||||
*
|
||||
* @param type 字典类型编号(1=部门类别)
|
||||
* @return List<Map> 每项 {k, v}
|
||||
*/
|
||||
List<Map<String, Object>> getSysDic(Integer type);
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。
|
||||
*
|
||||
* @param type 字典类型编号(0=部门类别)
|
||||
* @return List<Map>
|
||||
*/
|
||||
List<Map<String, Object>> getDictMangeList(String type);
|
||||
|
||||
/**
|
||||
* 添加发文单位。
|
||||
*
|
||||
* @param name 单位名称
|
||||
*/
|
||||
void addDeptDic(String name);
|
||||
|
||||
/**
|
||||
* 更新发文单位。
|
||||
*
|
||||
* @param serID 记录ID
|
||||
* @param name 名称
|
||||
* @param enable 启用状态
|
||||
*/
|
||||
void updateDeptDic(Integer serID, String name, Integer enable);
|
||||
|
||||
/**
|
||||
* 删除发文单位(校验是否被通知引用)。
|
||||
*
|
||||
* @param serID 记录ID
|
||||
*/
|
||||
void delDept(Integer serID);
|
||||
|
||||
/**
|
||||
* 发文单位排序。
|
||||
*
|
||||
* @param ids 逗号分隔的 SerID 列表(按前端顺序)
|
||||
*/
|
||||
void ordbyDicIds(String ids);
|
||||
|
||||
/**
|
||||
* 是否发送短信(查询当前学校模块短信开关)。
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean getSchModuleIsSms();
|
||||
|
||||
/**
|
||||
* 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。
|
||||
* 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。
|
||||
*
|
||||
* @param deptId 通知 deptId(存的是 sys_dict.dict_key)
|
||||
* @return 部门名称;无匹配时返回 null
|
||||
*/
|
||||
String resolveDeptName(String deptId);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import com.yunhai.campusnotice.entity.UpFile;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileVo;
|
||||
import com.yunhai.campusnotice.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);
|
||||
|
||||
/**
|
||||
* 批量上传图片(循环上传,返回每个文件的视图对象列表)。
|
||||
*/
|
||||
List<UpFileVo> uploadImages(MultipartFile[] files, String appType);
|
||||
|
||||
/**
|
||||
* 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。
|
||||
*/
|
||||
UpFileResultVo uploadToResult(MultipartFile file, String appType);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import com.yunhai.campusnotice.entity.Notice;
|
||||
import com.yunhai.campusnotice.model.BasePageResult;
|
||||
import com.yunhai.campusnotice.model.notice.*;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知公告业务接口。
|
||||
*/
|
||||
public interface INoticeService {
|
||||
|
||||
/**
|
||||
* 发通知:以当前登录用户为发布人入库。
|
||||
* 按 request.publishType 分流(改造方案 4.1):
|
||||
* direct/空 = 直接发布(兼容旧 Create 链路;携带未来 timingSendTime 时置 status=1 待调度);
|
||||
* draft = 存为草稿;approve = 提交审批(request.approverTeaIds 选教师,或签)。
|
||||
*
|
||||
* @return 新建通知 ID(用于后续回填附件 TopicID 等)
|
||||
*/
|
||||
Integer publish(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 存为草稿(status=0):仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人。
|
||||
*/
|
||||
Integer saveDraft(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 草稿箱列表(status=0,本人草稿):返回重要程度 type/typeDesc、定时标注与 timingSendTime。
|
||||
*/
|
||||
BasePageResult<NoticeVo> draftList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签),
|
||||
* 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。
|
||||
*/
|
||||
void submit(Integer serid, List<Integer> approverTeaIds, String opinion);
|
||||
|
||||
/**
|
||||
* 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布;
|
||||
* 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。
|
||||
*/
|
||||
void approve(Integer serid, String opinion);
|
||||
|
||||
/**
|
||||
* 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。
|
||||
*/
|
||||
void reject(Integer serid, String opinion);
|
||||
|
||||
/**
|
||||
* 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。
|
||||
*
|
||||
* @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理)
|
||||
*/
|
||||
BasePageResult<ApprovalListVo> approvalList(String auditState, NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。
|
||||
*/
|
||||
ApproverSuggestVo approverSuggest();
|
||||
|
||||
/**
|
||||
* 审批教师绑定:按学校教师列表(bschclass)绑定教师树选中ID → 姓名/头像。
|
||||
* schId 为空取当前登录人学校;未命中的ID保留、name 为 null 由调用方提示。
|
||||
*/
|
||||
List<ApproverResolveVo> approverResolve(List<Integer> ids, Integer schId);
|
||||
|
||||
/**
|
||||
* 编辑通知:草稿/已驳回/已发布(旧链路,无推送范围)可编辑;
|
||||
* 待审批禁止编辑;新版表单已发布(存在范围快照)禁止编辑(先撤回后重发)。
|
||||
*/
|
||||
void edit(Integer id, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 批量删除通知:仅草稿、已驳回、已发布(本人)可删,待审批不可删;
|
||||
* 级联清理阅读记录、接收人、推送范围、审批记录、附件关联。
|
||||
*/
|
||||
void deleteBatch(List<Integer> ids);
|
||||
|
||||
/**
|
||||
* 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。
|
||||
*/
|
||||
void pull(Integer id);
|
||||
|
||||
/**
|
||||
* 置顶/取消置顶。
|
||||
*/
|
||||
void setTopmost(Integer id, Integer topmost);
|
||||
|
||||
/**
|
||||
* 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。
|
||||
*/
|
||||
Integer forward(Integer sourceId, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 用户端列表:仅展示已发布、已到点、且对当前账号可见的通知。
|
||||
* 可见性(改造方案 3.2):无推送范围维持「全校广播」现状;
|
||||
* 存在范围的通知须 notice_receiver 快照命中当前账号(学员 receive_type=2 / 教师 receive_type=1)。
|
||||
*/
|
||||
NoticePageResult<NoticeVo> userList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 管理端列表:按条件查询全部通知(管理员查看全部)。
|
||||
*/
|
||||
BasePageResult<NoticeVo> adminList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 学员推送范围树(学段→年级→班级 + 全校根,数据源 bschclass),
|
||||
* 供表单「推送范围」胶囊按钮渲染。
|
||||
*/
|
||||
List<Map<String, Object>> scopeTree();
|
||||
|
||||
/**
|
||||
* 按已选范围预估接收人数(学员=范围展开去重账号数 + 教师=receiveList 定向名单数)。
|
||||
*/
|
||||
ScopePreviewVo scopePreview(List<PushScopeItem> scopes, String receiveList);
|
||||
|
||||
/**
|
||||
* 已发送通知阅读回执:按 年级→班级→学生 分组返回(身份快照),
|
||||
* 学生带已读/未读状态与阅读时间(前端着色),教师接收行归「教师」组。
|
||||
*/
|
||||
ReadReceiptVo readReceipt(Integer serid);
|
||||
|
||||
/**
|
||||
* 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。
|
||||
* 草稿/待审批/已驳回态仅发布人本人或该轮审批人可打开(改造方案 3.2 访问校验补口)。
|
||||
*/
|
||||
NoticeVo detail(Integer id);
|
||||
|
||||
/**
|
||||
* 编辑回显详情(不标记已读,访问校验同 {@link #detail})。
|
||||
*/
|
||||
NoticeVo getDetail(Integer id);
|
||||
|
||||
/**
|
||||
* 标记已读:为当前用户写入(幂等)该通知的阅读记录,
|
||||
* 学员阅读时按接收快照(缺失时 bschclass 实时兜底)写入学校/年级/班级身份快照。
|
||||
*/
|
||||
void markRead(Integer id);
|
||||
|
||||
/**
|
||||
* 一键全部已读(批量化):当前用户所有可见的已发布通知批量插入阅读记录。
|
||||
*
|
||||
* @return 已读条数
|
||||
*/
|
||||
int initializeRed();
|
||||
|
||||
/**
|
||||
* 追加接收人(事务化):并入接收人列表并重算 allowcount(范围通知含学员数)。
|
||||
*/
|
||||
void receiveUsers(Integer noticeId, String users);
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇通知ID(SQL 相邻查询)。
|
||||
*
|
||||
* @param serid 当前通知ID
|
||||
* @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列)
|
||||
* @return {prevId, nextId}
|
||||
*/
|
||||
Map<String, Object> getNextPrev(Integer serid, String type);
|
||||
|
||||
/**
|
||||
* 获取某通知的阅读明细列表。
|
||||
*/
|
||||
List<NoticeVo.ReadDetail> getReadDetail(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 阅读情况分组(已读/未读,旧接口保留过渡;新版表单走 {@link #readReceipt})。
|
||||
*/
|
||||
List<Map<String, Object>> getReadDetailGrouped(Integer serid);
|
||||
|
||||
/**
|
||||
* 催收未读:仅 receiptFlag=1 的通知可用;
|
||||
* 催收对象优先 notice_receiver 快照(含学员),回退 receiveList JSON(历史数据)。
|
||||
*/
|
||||
void expedite(Integer serid);
|
||||
|
||||
/**
|
||||
* 定时发送调度(TimingPublishJob 每分钟触发):
|
||||
* 扫描 status=1 且 timingSendTime<=now 且 timing_published=0 的通知,
|
||||
* 逐条 CAS 抢占(幂等)后执行真实发布。
|
||||
*
|
||||
* @return 本次真实发布条数
|
||||
*/
|
||||
int timingPublishDue();
|
||||
|
||||
/**
|
||||
* 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。
|
||||
*/
|
||||
NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 通知详情视图(含附件、接收人、推送范围、审批轨迹),供 Controller 返回。
|
||||
*/
|
||||
NoticeDetailVo getDetailMap(Integer serid);
|
||||
|
||||
/**
|
||||
* 已发列表附件批量查询(消除 N+1)。
|
||||
*/
|
||||
Map<Integer, List<Integer>> batchFileIds(List<Integer> noticeIds);
|
||||
|
||||
/**
|
||||
* 查看通知详情视图(含附件、接收人)。
|
||||
* type=1 时先标记已读再返回,type=0 时直接返回不标记。
|
||||
*/
|
||||
NoticeDetailVo detailMap(Integer serid, Integer type);
|
||||
|
||||
/**
|
||||
* 一键全部已读并返回结果(count = 已读条数)。
|
||||
*/
|
||||
InitializeRedVo initializeRedResult();
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。
|
||||
*/
|
||||
void downloadAllFiles(Integer dataid, HttpServletResponse response);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信设置业务接口。
|
||||
* 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。
|
||||
*/
|
||||
public interface ISmsService {
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
* C# 从 AuthConfig.AppKeyMap 读取。Java版从 sms_module 表读取。
|
||||
*
|
||||
* @return List<Map> 每项 {ID, Name}
|
||||
*/
|
||||
List<Map<String, Object>> getModuleList();
|
||||
|
||||
/**
|
||||
* 获取学校父类组列表。
|
||||
* C# 从外部BLL获取学校组。Java版预留。
|
||||
*
|
||||
* @return List<Map> 每项 {ID, Name}
|
||||
*/
|
||||
List<Map<String, Object>> getSchParentGroup();
|
||||
|
||||
/**
|
||||
* 分页获取学校短信开关列表。
|
||||
*
|
||||
* @param pageSize 每页条数
|
||||
* @param pageIndex 页码
|
||||
* @param schName 学校名称(模糊搜索)
|
||||
* @return Map: { list, total }
|
||||
*/
|
||||
Map<String, Object> getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName);
|
||||
|
||||
/**
|
||||
* 获取学校列表。
|
||||
* C# 从外部BLL获取学校列表。Java版预留。
|
||||
*
|
||||
* @return List<Map> 每项 {ID, Name}
|
||||
*/
|
||||
List<Map<String, Object>> getSchList();
|
||||
|
||||
/**
|
||||
* 设置单校单模块短信开关。
|
||||
*
|
||||
* @param schSerID 学校ID
|
||||
* @param moduleId 模块ID
|
||||
* @param isSms 是否开通
|
||||
*/
|
||||
void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms);
|
||||
|
||||
/**
|
||||
* 设置某校全部模块短信开关。
|
||||
*
|
||||
* @param schSerID 学校ID
|
||||
* @param isSms 是否开通
|
||||
*/
|
||||
void setSchSmsAll(Integer schSerID, Boolean isSms);
|
||||
|
||||
/**
|
||||
* 批量设置多校多模块短信开关。
|
||||
*
|
||||
* @param schSerIDs 逗号分隔的学校ID
|
||||
* @param modules 逗号分隔的模块ID
|
||||
* @param isSms 是否开通
|
||||
*/
|
||||
void setSchModuleSms(String schSerIDs, String modules, Boolean isSms);
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串解析并设置单校单模块短信开关。
|
||||
* 解析 SchSerID/ModuID/IsSms 三个字段。
|
||||
*/
|
||||
void setSchSmsFromJson(String itemsModuJson);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.yunhai.campusnotice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知模板业务接口。
|
||||
* 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。
|
||||
*/
|
||||
public interface ITemplateService {
|
||||
|
||||
/**
|
||||
* 获取模板类型列表(首项固定为"全部")。
|
||||
*
|
||||
* @return List<Map> 每项 {TypeID, TypeName}
|
||||
*/
|
||||
List<Map<String, Object>> getTypelist();
|
||||
|
||||
/**
|
||||
* 新增/编辑模板类型。
|
||||
*
|
||||
* @param typeid 类型ID(0=新增)
|
||||
* @param typeName 类型名称
|
||||
*/
|
||||
void editType(Integer typeid, String typeName);
|
||||
|
||||
/**
|
||||
* 删除模板类型(校验是否有模板引用此类型)。
|
||||
*
|
||||
* @param typeid 类型ID
|
||||
*/
|
||||
void delType(Integer typeid);
|
||||
|
||||
/**
|
||||
* 分页获取模板列表。
|
||||
*
|
||||
* @param pageSize 每页条数
|
||||
* @param pageIndex 页码
|
||||
* @param typeid 类型ID(0=全部)
|
||||
* @return Map: { list, total }
|
||||
*/
|
||||
Map<String, Object> getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid);
|
||||
|
||||
/**
|
||||
* 新增/编辑模板。
|
||||
*
|
||||
* @param serID 模板ID(null=新增)
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
* @param typeId 类型ID
|
||||
* @param cover 封面文件ID
|
||||
*/
|
||||
void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover);
|
||||
|
||||
/**
|
||||
* 删除模板。
|
||||
*
|
||||
* @param serID 模板ID
|
||||
*/
|
||||
void delTemplate(Integer serID);
|
||||
|
||||
/**
|
||||
* 获取单个模板详情。
|
||||
*
|
||||
* @param serID 模板ID
|
||||
* @return Map 模板详情
|
||||
*/
|
||||
Map<String, Object> getTemplateFist(Integer serID);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.yunhai.campusnotice.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,176 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.model.SchTeaList;
|
||||
import com.yunhai.campusnotice.model.bsch.SchClassDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchGradeDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchInfoDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchStudentDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchSystemDto;
|
||||
import com.yunhai.campusnotice.model.bsch.SchTreeNodeDto;
|
||||
import com.yunhai.campusnotice.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)开放接口实现。
|
||||
* bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。
|
||||
* <p>推送范围树/圈人/身份快照统一经本门面(改造方案 2.0):
|
||||
* 学段 GetSchLenlist、年级 GetSchLenLevellist、班级 GetVClassList、
|
||||
* 学生 GetSchStulist / GetGradeStulist(补缺) / GetSubStulist(补缺) / GetClaArrStulist / GetStuIdslist、
|
||||
* 学生树 SchGroup/GetStuTree。
|
||||
*/
|
||||
@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] 请求班级管理接口失败, url={}", url, e);
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取学校信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchSystemDto> getSchLenList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenlist?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchSystemDto>>() {
|
||||
}, "获取学段列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchGradeDto> getSchGradeList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/Schoolen/GetSchLenLevellist?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchGradeDto>>() {
|
||||
}, "获取年级列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchClassDto> getSchClassList(String schSerId) {
|
||||
return exchangeList(bschClassHost + "/oapi/VSchClass/GetVClassList?SchSerID=" + schSerId,
|
||||
new ParameterizedTypeReference<List<SchClassDto>>() {
|
||||
}, "获取班级列表", "schSerId=" + schSerId);
|
||||
}
|
||||
|
||||
@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> getGradeStuList(String schSerId, Integer gradeId) {
|
||||
return exchangeList(bschClassHost + "/oapi/OAStu/GetGradeStulist?SchSerID=" + schSerId + "&GradeId=" + gradeId,
|
||||
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||
}, "获取年级学生", "schSerId=" + schSerId + ", gradeId=" + gradeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchStudentDto> getSubStuList(String schSerId, Integer schSystemId) {
|
||||
return exchangeList(bschClassHost + "/oapi/OAStu/GetSubStulist?SchSerID=" + schSerId + "&SchSystemId=" + schSystemId,
|
||||
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||
}, "获取学段学生", "schSerId=" + schSerId + ", schSystemId=" + schSystemId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchStudentDto> getClaArrStuList(List<Integer> classIds) {
|
||||
String csv = joinIds(classIds);
|
||||
return exchangeList(bschClassHost + "/oapi/OAStu/GetClaArrStulist?ClassIDs=" + csv,
|
||||
new ParameterizedTypeReference<List<SchStudentDto>>() {
|
||||
}, "获取班级学生", "classIds=" + csv);
|
||||
}
|
||||
|
||||
@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<SchTreeNodeDto> getStuTree(Integer schId) {
|
||||
return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId,
|
||||
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
||||
}, "获取学生树", "schId=" + schId);
|
||||
}
|
||||
|
||||
// ============================ 通用调用 ============================
|
||||
|
||||
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
|
||||
String action, String arg) {
|
||||
requireHost();
|
||||
try {
|
||||
// GET + Accept: application/json,保持与既有 GetSchteaList 调用一致
|
||||
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,413 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.entity.ContactsGroup;
|
||||
import com.yunhai.campusnotice.entity.ContactsGroupUser;
|
||||
import com.yunhai.campusnotice.repository.ContactsGroupRepository;
|
||||
import com.yunhai.campusnotice.repository.ContactsGroupUserRepository;
|
||||
import com.yunhai.campusnotice.service.IContactsService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.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.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
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;
|
||||
}
|
||||
String account = currentAccount();
|
||||
|
||||
// 查重:同创建人下同名同类型组不重复
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
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.setCreatorAccount(account);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
|
||||
// ============================ GetGroup ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getGroup(Integer page, Integer count, Integer type) {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = currentAccount();
|
||||
|
||||
// 查询当前用户创建的全部联系组
|
||||
List<ContactsGroup> allGroups = groupRepository.findByCreatorAccount(account);
|
||||
|
||||
// 统计每组人数
|
||||
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, "参数错误!");
|
||||
}
|
||||
String account = currentAccount();
|
||||
|
||||
// 查重(排除自身)
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
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.setUserAccount(user.getAccount());
|
||||
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 的行为。
|
||||
* 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。
|
||||
*/
|
||||
private void fillUserInfo(Map<String, Object> item, ContactsGroupUser m) {
|
||||
String name = m.getUserName();
|
||||
String account = m.getUserAccount();
|
||||
if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) {
|
||||
BoaUser user = null;
|
||||
try {
|
||||
user = YschBaseOpenApi.getUserInfo(m.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
if (user == null) {
|
||||
log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId());
|
||||
} else {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
account = user.getAccount();
|
||||
}
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = user.getName();
|
||||
}
|
||||
// 回写落库:下次读取直接命中快照
|
||||
if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account))
|
||||
|| (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) {
|
||||
if (StringUtils.hasText(account)) {
|
||||
m.setUserAccount(account);
|
||||
}
|
||||
if (StringUtils.hasText(name)) {
|
||||
m.setUserName(name);
|
||||
}
|
||||
try {
|
||||
groupUserRepository.save(m);
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item.put("name", name != null ? name : "");
|
||||
item.put("account", account != null ? account : "");
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getGroupTree(Integer type) {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = currentAccount();
|
||||
|
||||
// 查询当前用户的联系组
|
||||
List<ContactsGroup> groups = groupRepository.findByCreatorAccount(account);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
|
||||
*/
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.entity.Notice;
|
||||
import com.yunhai.campusnotice.entity.SysDict;
|
||||
import com.yunhai.campusnotice.repository.NoticeRepository;
|
||||
import com.yunhai.campusnotice.repository.SysDictRepository;
|
||||
import com.yunhai.campusnotice.service.IDictService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 系统字典服务实现。
|
||||
* 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。
|
||||
*/
|
||||
@Service
|
||||
public class DictService implements IDictService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DictService.class);
|
||||
|
||||
/** 字典类型常量:发文部门 */
|
||||
private static final String DICT_TYPE_DEPT = "dept";
|
||||
|
||||
@Autowired
|
||||
private SysDictRepository sysDictRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
// ============================ GetSysDic ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSysDic(Integer type) {
|
||||
// C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及
|
||||
String dictType = mapDictType(type);
|
||||
if (dictType == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1);
|
||||
if (list.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!");
|
||||
}
|
||||
// 按 sortOrder 升序返回 {k, v}
|
||||
return list.stream()
|
||||
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? Integer.MAX_VALUE : d.getSortOrder()))
|
||||
.map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("k", d.getDictKey());
|
||||
item.put("v", d.getDictValue());
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ GetDictMangeList ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getDictMangeList(String type) {
|
||||
// type="dept" 表示部门类别(DEPT)
|
||||
String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null;
|
||||
if (dictType == null) {
|
||||
// 非 DEPT 类型需超管权限,本系统暂只支持 DEPT
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType);
|
||||
return list.stream()
|
||||
.sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()))
|
||||
.map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("SerID", d.getId());
|
||||
item.put("CodeID", d.getDictKey());
|
||||
item.put("Name", d.getDictValue());
|
||||
item.put("Des", d.getSortOrder() != null ? d.getSortOrder().toString() : "");
|
||||
item.put("Enable", d.getEnableFlag());
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ AddDeptDic ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void addDeptDic(String name) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!");
|
||||
}
|
||||
// 校验重名
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
DICT_TYPE_DEPT.equals(d.getDictType()) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
// 计算 sortOrder = max + 1
|
||||
int maxSort = existing.stream()
|
||||
.filter(d -> d.getEnableFlag() != 3)
|
||||
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : 0)
|
||||
.max().orElse(0);
|
||||
|
||||
SysDict dict = new SysDict();
|
||||
dict.setDictType(DICT_TYPE_DEPT);
|
||||
dict.setDictValue(name);
|
||||
dict.setSortOrder(maxSort + 1);
|
||||
dict.setDictKey(DICT_TYPE_DEPT + (maxSort + 1));
|
||||
dict.setEnableFlag(1);
|
||||
sysDictRepository.save(dict);
|
||||
}
|
||||
|
||||
// ============================ UpdateDeptDic ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateDeptDic(Integer serID, String name, Integer enable) {
|
||||
if (serID == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
// 校验重名(排除自身)
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
!d.getId().equals(serID) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
if (StringUtils.hasText(name)) {
|
||||
dict.setDictValue(name);
|
||||
}
|
||||
if (enable != null) {
|
||||
dict.setEnableFlag(enable);
|
||||
}
|
||||
sysDictRepository.save(dict);
|
||||
}
|
||||
|
||||
// ============================ DelDept ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delDept(Integer serID) {
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
// 校验是否被通知引用(按 deptId = dictKey 匹配)
|
||||
// C# 中检查 Notify_Message.DeptID == dict.CodeID
|
||||
// Java 中 Notice.deptId 存的是 dictKey
|
||||
List<Notice> referenced = noticeRepository.findAll((root, query, cb) ->
|
||||
cb.equal(root.get("deptId"), dict.getDictKey()));
|
||||
if (!referenced.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已引用该部门,无法删除!");
|
||||
}
|
||||
sysDictRepository.delete(dict);
|
||||
}
|
||||
|
||||
// ============================ OrdbyDicIds ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void ordbyDicIds(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
if (list.isEmpty()) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
// C# 逻辑:取当前最大 Sort 为起始序号,逆序遍历 ids 递减赋值 Des
|
||||
int[] idArr = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.mapToInt(Integer::parseInt)
|
||||
.toArray();
|
||||
|
||||
int maxSort = list.stream()
|
||||
.mapToInt(d -> d.getSortOrder() != null ? d.getSortOrder() : idArr.length)
|
||||
.max().orElse(idArr.length);
|
||||
|
||||
// lambda 中不能修改局部变量,用数组包装
|
||||
final int[] indexOrd = { maxSort };
|
||||
|
||||
// 逆序遍历,从最大序号递减
|
||||
for (int i = idArr.length - 1; i >= 0; i--) {
|
||||
int id = idArr[i];
|
||||
sysDictRepository.findById(id).ifPresent(dict -> {
|
||||
dict.setSortOrder(indexOrd[0]);
|
||||
sysDictRepository.save(dict);
|
||||
indexOrd[0]--;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ GetSchModuleIsSms ============================
|
||||
|
||||
@Override
|
||||
public Boolean getSchModuleIsSms() {
|
||||
// C# 通过外部 ISchIsSmsBLL 查询学校短信开关。
|
||||
// Java 版暂不接入外部短信平台,默认返回 false。
|
||||
String account = currentAccount();
|
||||
if (account == null) {
|
||||
return false;
|
||||
}
|
||||
// 后续接入短信平台后补全查询逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 按发文部门字典 key 解析部门名称(对应 C# GetDeptName)。
|
||||
*/
|
||||
@Override
|
||||
public String resolveDeptName(String deptId) {
|
||||
if (!StringUtils.hasText(deptId)) {
|
||||
return null;
|
||||
}
|
||||
return sysDictRepository.findByDictTypeAndDictKey(DICT_TYPE_DEPT, deptId.trim())
|
||||
.map(SysDict::getDictValue)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端 Type 参数映射到字典类型字符串。
|
||||
* C# 中使用枚举,Java 版仅支持 DEPT(1)。
|
||||
*/
|
||||
private String mapDictType(Integer type) {
|
||||
if (type == null) return null;
|
||||
switch (type) {
|
||||
case 1: return DICT_TYPE_DEPT;
|
||||
// 其余类型(文印/请假/物品等)本系统不涉及
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
|
||||
*/
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.entity.UpFile;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileVo;
|
||||
import com.yunhai.campusnotice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.campusnotice.repository.UpFileRepository;
|
||||
import com.yunhai.campusnotice.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 = "notice";
|
||||
|
||||
// ============================ 上传 ============================
|
||||
|
||||
@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
|
||||
@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,284 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.entity.SmsModule;
|
||||
import com.yunhai.campusnotice.entity.SmsSchoolConfig;
|
||||
import com.yunhai.campusnotice.repository.SmsModuleRepository;
|
||||
import com.yunhai.campusnotice.repository.SmsSchoolConfigRepository;
|
||||
import com.yunhai.campusnotice.service.ISmsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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# SchModuleSmsController + SchModuleSms.cs。
|
||||
* 注意:C# 原版依赖外部DLL(ISchIsSmsBLL/ISchoolBLL/ISchGroupBLL)获取学校和组数据,
|
||||
* Java版改为从本地 sms_module + sms_school_config 表管理。
|
||||
*/
|
||||
@Service
|
||||
public class SmsService implements ISmsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SmsService.class);
|
||||
|
||||
@Autowired
|
||||
private SmsModuleRepository smsModuleRepository;
|
||||
|
||||
@Autowired
|
||||
private SmsSchoolConfigRepository smsSchoolConfigRepository;
|
||||
|
||||
// ============================ getmodule ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getModuleList() {
|
||||
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
|
||||
return modules.stream().map(m -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("ID", m.getId());
|
||||
item.put("Name", m.getModuleName());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ getgroup ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSchParentGroup() {
|
||||
// C# 从外部BLL获取学校组数据。Java版暂不接入用户中心API。
|
||||
// 返回空列表,前端可按需处理。
|
||||
log.info("[getSchParentGroup] 暂未接入用户中心API,返回空列表");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ getschsms ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName) {
|
||||
if (pageSize == null || pageSize <= 0) pageSize = 10;
|
||||
// 前端 PageIndex 从0开始(page-1),后端按0基处理
|
||||
if (pageIndex == null || pageIndex < 0) pageIndex = 0;
|
||||
|
||||
// 查询学校配置(按学校名模糊搜索或全部)
|
||||
Page<SmsSchoolConfig> page;
|
||||
int offset = pageIndex; // 0基
|
||||
Sort sort = Sort.by(Sort.Direction.DESC, "schoolName");
|
||||
|
||||
if (StringUtils.hasText(schName)) {
|
||||
page = smsSchoolConfigRepository.findBySchoolNameContaining(schName,
|
||||
PageRequest.of(offset, pageSize, sort));
|
||||
} else {
|
||||
page = smsSchoolConfigRepository.findAll(PageRequest.of(offset, pageSize, sort));
|
||||
}
|
||||
|
||||
// 加载全部模块列表
|
||||
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
|
||||
|
||||
// 按 schoolId 分组配置
|
||||
Map<Integer, List<SmsSchoolConfig>> configBySchool = page.getContent().stream()
|
||||
.collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId));
|
||||
|
||||
// 构建返回数据:每个学校一行,包含各模块的开关状态
|
||||
List<Map<String, Object>> listData = page.getContent().stream()
|
||||
.collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId))
|
||||
.entrySet().stream()
|
||||
.map(entry -> {
|
||||
Integer schoolId = entry.getKey();
|
||||
List<SmsSchoolConfig> configs = entry.getValue();
|
||||
String schoolName = configs.get(0).getSchoolName();
|
||||
|
||||
// 构建模块开关矩阵
|
||||
List<Map<String, Object>> moduleList = new ArrayList<>();
|
||||
boolean isAll = true;
|
||||
for (SmsModule module : modules) {
|
||||
Map<String, Object> modu = new LinkedHashMap<>();
|
||||
SmsSchoolConfig config = configs.stream()
|
||||
.filter(c -> module.getId().equals(c.getModuleId()))
|
||||
.findFirst().orElse(null);
|
||||
modu.put("SerID", config != null ? config.getId() : -1);
|
||||
modu.put("ModuID", module.getId());
|
||||
modu.put("IsSms", config != null && Boolean.TRUE.equals(config.getIsSms()));
|
||||
modu.put("SchSerID", schoolId);
|
||||
moduleList.add(modu);
|
||||
if (config == null || !Boolean.TRUE.equals(config.getIsSms())) {
|
||||
isAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> schoolItem = new LinkedHashMap<>();
|
||||
schoolItem.put("SchSerID", schoolId);
|
||||
schoolItem.put("SchName", schoolName);
|
||||
schoolItem.put("IsAll", isAll);
|
||||
schoolItem.put("itemsModu", moduleList);
|
||||
return schoolItem;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 如果没有配置数据,返回空列表(但保留total)
|
||||
if (listData.isEmpty() && page.hasContent()) {
|
||||
// 这种情况不应发生,但防御性处理
|
||||
listData = Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", page.getTotalElements());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ getsch ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSchList() {
|
||||
// C# 从外部BLL获取学校列表。Java版从 sms_school_config 表提取已配置学校。
|
||||
// 获取所有不重复的学校
|
||||
List<SmsSchoolConfig> allConfigs = smsSchoolConfigRepository.findAll();
|
||||
Map<Integer, String> schoolMap = new LinkedHashMap<>();
|
||||
for (SmsSchoolConfig config : allConfigs) {
|
||||
schoolMap.putIfAbsent(config.getSchoolId(), config.getSchoolName());
|
||||
}
|
||||
|
||||
return schoolMap.entrySet().stream().map(entry -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("ID", entry.getKey());
|
||||
item.put("Name", entry.getValue());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ setschsms ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSchSms(Integer schSerID, Integer moduleId, Boolean isSms) {
|
||||
if (schSerID == null || moduleId == null) {
|
||||
return;
|
||||
}
|
||||
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schSerID, moduleId);
|
||||
if (config == null) {
|
||||
config = new SmsSchoolConfig();
|
||||
config.setSchoolId(schSerID);
|
||||
config.setModuleId(moduleId);
|
||||
// 学校名称从已有配置中获取
|
||||
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schSerID);
|
||||
if (!existing.isEmpty()) {
|
||||
config.setSchoolName(existing.get(0).getSchoolName());
|
||||
}
|
||||
}
|
||||
config.setIsSms(isSms != null ? isSms : false);
|
||||
smsSchoolConfigRepository.save(config);
|
||||
}
|
||||
|
||||
// ============================ setschallsms ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSchSmsAll(Integer schSerID, Boolean isSms) {
|
||||
if (schSerID == null) {
|
||||
return;
|
||||
}
|
||||
List<SmsModule> modules = smsModuleRepository.findAllByOrderByIdAsc();
|
||||
boolean sms = Boolean.TRUE.equals(isSms);
|
||||
|
||||
for (SmsModule module : modules) {
|
||||
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schSerID, module.getId());
|
||||
if (config == null) {
|
||||
config = new SmsSchoolConfig();
|
||||
config.setSchoolId(schSerID);
|
||||
config.setModuleId(module.getId());
|
||||
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schSerID);
|
||||
if (!existing.isEmpty()) {
|
||||
config.setSchoolName(existing.get(0).getSchoolName());
|
||||
}
|
||||
}
|
||||
config.setIsSms(sms);
|
||||
smsSchoolConfigRepository.save(config);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ setschmodulesms ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSchModuleSms(String schSerIDs, String modules, Boolean isSms) {
|
||||
if (!StringUtils.hasText(schSerIDs) || !StringUtils.hasText(modules)) {
|
||||
return;
|
||||
}
|
||||
List<Integer> schoolIds = Arrays.stream(schSerIDs.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
List<Integer> moduleIds = Arrays.stream(modules.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
boolean sms = Boolean.TRUE.equals(isSms);
|
||||
|
||||
for (Integer schoolId : schoolIds) {
|
||||
for (Integer moduleId : moduleIds) {
|
||||
SmsSchoolConfig config = smsSchoolConfigRepository.findBySchoolIdAndModuleId(schoolId, moduleId);
|
||||
if (config == null) {
|
||||
config = new SmsSchoolConfig();
|
||||
config.setSchoolId(schoolId);
|
||||
config.setModuleId(moduleId);
|
||||
List<SmsSchoolConfig> existing = smsSchoolConfigRepository.findBySchoolId(schoolId);
|
||||
if (!existing.isEmpty()) {
|
||||
config.setSchoolName(existing.get(0).getSchoolName());
|
||||
}
|
||||
}
|
||||
config.setIsSms(sms);
|
||||
smsSchoolConfigRepository.save(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ setschsms (JSON 解析) ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSchSmsFromJson(String itemsModuJson) {
|
||||
Integer schSerID = extractJsonField(itemsModuJson, "SchSerID");
|
||||
Integer moduID = extractJsonField(itemsModuJson, "ModuID");
|
||||
Boolean isSms = extractJsonBool(itemsModuJson, "IsSms");
|
||||
setSchSms(schSerID, moduID, isSms);
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取整数字段。
|
||||
*/
|
||||
private Integer extractJsonField(String json, String field) {
|
||||
if (json == null) return null;
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*\"?(-?\\d+)\"?";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Integer.parseInt(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取布尔字段。
|
||||
*/
|
||||
private Boolean extractJsonBool(String json, String field) {
|
||||
if (json == null) return null;
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*(true|false)";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Boolean.parseBoolean(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeTemplate;
|
||||
import com.yunhai.campusnotice.entity.TemplateType;
|
||||
import com.yunhai.campusnotice.entity.UpFile;
|
||||
import com.yunhai.campusnotice.repository.NoticeTemplateRepository;
|
||||
import com.yunhai.campusnotice.repository.TemplateTypeRepository;
|
||||
import com.yunhai.campusnotice.repository.UpFileRepository;
|
||||
import com.yunhai.campusnotice.service.ITemplateService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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# TemplateController + Template.cs。
|
||||
*/
|
||||
@Service
|
||||
public class TemplateService implements ITemplateService {
|
||||
|
||||
@Autowired
|
||||
private TemplateTypeRepository templateTypeRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeTemplateRepository noticeTemplateRepository;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
// ============================ GetTypelist ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getTypelist() {
|
||||
List<TemplateType> types = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
// 首项固定为"全部"
|
||||
Map<String, Object> allItem = new LinkedHashMap<>();
|
||||
allItem.put("TypeID", 0);
|
||||
allItem.put("TypeName", "全部");
|
||||
result.add(allItem);
|
||||
|
||||
for (TemplateType t : types) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("TypeID", t.getId());
|
||||
item.put("TypeName", t.getTypeName());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ EditType ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void editType(Integer typeid, String typeName) {
|
||||
if (!StringUtils.hasText(typeName)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "类型名称不能为空!");
|
||||
}
|
||||
// 重名校验
|
||||
boolean exists;
|
||||
if (typeid == null || typeid == 0) {
|
||||
exists = templateTypeRepository.existsByTypeName(typeName);
|
||||
} else {
|
||||
exists = templateTypeRepository.existsByTypeNameAndIdNot(typeName, typeid);
|
||||
}
|
||||
if (exists) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "类型名称已存在!");
|
||||
}
|
||||
|
||||
if (typeid == null || typeid == 0) {
|
||||
// 新增
|
||||
TemplateType type = new TemplateType();
|
||||
type.setTypeName(typeName);
|
||||
// sortOrder = max + 1
|
||||
List<TemplateType> all = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
int maxSort = all.stream()
|
||||
.mapToInt(t -> t.getSortOrder() != null ? t.getSortOrder() : 0)
|
||||
.max().orElse(0);
|
||||
type.setSortOrder(maxSort + 1);
|
||||
templateTypeRepository.save(type);
|
||||
} else {
|
||||
// 编辑
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
|
||||
type.setTypeName(typeName);
|
||||
templateTypeRepository.save(type);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ DelType ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delType(Integer typeid) {
|
||||
if (typeid == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "类型不存在!");
|
||||
}
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
|
||||
// 校验是否有模板引用此类型
|
||||
long refCount = noticeTemplateRepository.countByTypeId(typeid);
|
||||
if (refCount > 0) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "类型存在模板!");
|
||||
}
|
||||
templateTypeRepository.delete(type);
|
||||
}
|
||||
|
||||
// ============================ GetTemplatelist ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid) {
|
||||
if (pageSize == null || pageSize <= 0) pageSize = 10;
|
||||
if (pageIndex == null || pageIndex <= 0) pageIndex = 1;
|
||||
if (typeid == null) typeid = 0;
|
||||
|
||||
Page<NoticeTemplate> page;
|
||||
int offset = pageIndex - 1;
|
||||
|
||||
if (typeid > 0) {
|
||||
page = noticeTemplateRepository.findByTypeId(typeid,
|
||||
PageRequest.of(offset, pageSize, Sort.by(Sort.Direction.DESC, "refCount")));
|
||||
} else {
|
||||
page = noticeTemplateRepository.findAllByOrderByRefCountDesc(PageRequest.of(offset, pageSize));
|
||||
}
|
||||
|
||||
// 加载类型名映射
|
||||
List<TemplateType> allTypes = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
Map<Integer, String> typeNameMap = allTypes.stream()
|
||||
.collect(Collectors.toMap(TemplateType::getId, TemplateType::getTypeName, (a, b) -> a));
|
||||
|
||||
// 加载封面图
|
||||
List<Integer> coverIds = page.getContent().stream()
|
||||
.map(NoticeTemplate::getCover)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Integer, String> coverUrlMap = new HashMap<>();
|
||||
if (!coverIds.isEmpty()) {
|
||||
for (Integer fid : coverIds) {
|
||||
upFileRepository.findById(fid).ifPresent(f -> coverUrlMap.put(fid, f.getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> listData = page.getContent().stream().map(t -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("SerID", t.getId());
|
||||
item.put("Title", t.getTitle());
|
||||
item.put("TypeName", typeNameMap.getOrDefault(t.getTypeId(), ""));
|
||||
item.put("Contents", t.getContent());
|
||||
item.put("ContentsFilter", filterHtml(t.getContent()));
|
||||
item.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
|
||||
item.put("imgurl", t.getCover() != null ? coverUrlMap.get(t.getCover()) : null);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", page.getTotalElements());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ EditTemplate ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover) {
|
||||
if (!StringUtils.hasText(title)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "模板标题不能为空!");
|
||||
}
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "模板内容不能为空!");
|
||||
}
|
||||
|
||||
if (serID == null || serID == 0) {
|
||||
// 新增
|
||||
NoticeTemplate template = new NoticeTemplate();
|
||||
template.setTitle(title);
|
||||
template.setContent(content);
|
||||
template.setTypeId(typeId);
|
||||
template.setCover(cover);
|
||||
template.setRefCount(0);
|
||||
noticeTemplateRepository.save(template);
|
||||
} else {
|
||||
// 编辑
|
||||
NoticeTemplate template = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
|
||||
template.setTitle(title);
|
||||
template.setContent(content);
|
||||
template.setTypeId(typeId);
|
||||
template.setCover(cover);
|
||||
noticeTemplateRepository.save(template);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ DelTemplate ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delTemplate(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
noticeTemplateRepository.findById(serID).ifPresent(noticeTemplateRepository::delete);
|
||||
}
|
||||
|
||||
// ============================ GetTemplateFist ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTemplateFist(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
NoticeTemplate t = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
|
||||
|
||||
// 加载类型名
|
||||
String typeName = "";
|
||||
if (t.getTypeId() != null) {
|
||||
Optional<TemplateType> type = templateTypeRepository.findById(t.getTypeId());
|
||||
if (type.isPresent()) {
|
||||
typeName = type.get().getTypeName();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载封面图
|
||||
String imgurl = null;
|
||||
if (t.getCover() != null) {
|
||||
Optional<UpFile> file = upFileRepository.findById(t.getCover());
|
||||
if (file.isPresent()) {
|
||||
imgurl = file.get().getFileName();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("SerID", t.getId());
|
||||
result.put("Title", t.getTitle());
|
||||
result.put("Type", t.getTypeId());
|
||||
result.put("TypeName", typeName);
|
||||
result.put("Contents", t.getContent());
|
||||
result.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
|
||||
result.put("imgurl", imgurl);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 过滤HTML标签,返回纯文本(对应 C# WebHelper.FilterHtml)。
|
||||
*/
|
||||
private String filterHtml(String html) {
|
||||
if (html == null) return "";
|
||||
return html.replaceAll("<[^>]+>", "").trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.yunhai.campusnotice.service.impl;
|
||||
|
||||
import com.yunhai.campusnotice.model.SchTeaList;
|
||||
import com.yunhai.campusnotice.service.IClassService;
|
||||
import com.yunhai.campusnotice.service.IUserCenterService;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user