Compare commits
53
Commits
9b2d628970
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
687bb65b3a | ||
|
|
9347ddb409 | ||
|
|
7c5391458c | ||
|
|
986a875279 | ||
|
|
d0af9d481c | ||
|
|
414dbfc2b2 | ||
|
|
d99ce81cc0 | ||
|
|
b96de59b6d | ||
|
|
a4e8702dc0 | ||
|
|
48cd8884d8 | ||
|
|
b85f5e16fb | ||
|
|
81a988f96a | ||
|
|
5efe9c09fd | ||
|
|
e6e91157be | ||
|
|
09eb27c6cf | ||
|
|
0721cf3b7f | ||
|
|
0d05986f8c | ||
|
|
867809fe37 | ||
|
|
a103d661fc | ||
|
|
0216d0655c | ||
|
|
082ef0af0a | ||
|
|
4ac3626b92 | ||
|
|
a7e93cb22d | ||
|
|
58aadaa95a | ||
|
|
df9b9e0aa5 | ||
|
|
5db629cfdb | ||
|
|
a5204680e6 | ||
|
|
08e9531b5f | ||
|
|
b3c5326163 | ||
|
|
8852c7ab02 | ||
|
|
01ce87ed7a | ||
|
|
bfb50a205d | ||
|
|
138b226ab0 | ||
|
|
fffcf1c66d | ||
|
|
69c2a41dd2 | ||
|
|
5384a08475 | ||
|
|
d5f25b8029 | ||
|
|
7cf17528ef | ||
|
|
2c8125a549 | ||
|
|
9351d6e9ab | ||
|
|
5cc6730941 | ||
|
|
41849defc9 | ||
|
|
c387f8eb76 | ||
|
|
6375562c8d | ||
|
|
83975ad93b | ||
|
|
88456e4fc8 | ||
|
|
379d073df2 | ||
|
|
81f32026c3 | ||
|
|
9114f44331 | ||
|
|
bf973e5979 | ||
|
|
40dd7fadea | ||
|
|
c892461405 | ||
|
|
4d86472913 |
@@ -25,7 +25,10 @@ jobs:
|
|||||||
options: --network infra-infra-net
|
options: --network infra-infra-net
|
||||||
steps:
|
steps:
|
||||||
- name: 安装 git / curl(alpine)
|
- name: 安装 git / curl(alpine)
|
||||||
run: apk add --no-cache git curl
|
run: |
|
||||||
|
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
|
||||||
|
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
|
||||||
|
apk add --no-cache git curl
|
||||||
|
|
||||||
# 内网 Gitea 无法访问 GitHub 拉 actions/checkout,手动 clone 本仓库
|
# 内网 Gitea 无法访问 GitHub 拉 actions/checkout,手动 clone 本仓库
|
||||||
- name: 拉取源码(内网无 GitHub,手动 clone 本仓库)
|
- name: 拉取源码(内网无 GitHub,手动 clone 本仓库)
|
||||||
@@ -55,18 +58,24 @@ jobs:
|
|||||||
- name: 构建 jar(maven 容器,复用宿主 m2 缓存)
|
- name: 构建 jar(maven 容器,复用宿主 m2 缓存)
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
docker rm -f mavenbuild 2>/dev/null || true
|
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
|
||||||
docker run -d --name mavenbuild \
|
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
|
||||||
|
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
|
||||||
|
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
|
||||||
|
BUILD_CONTAINER="mavenbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
|
||||||
|
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
|
||||||
|
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
|
||||||
|
docker run -d --name "$BUILD_CONTAINER" \
|
||||||
--network infra-infra-net \
|
--network infra-infra-net \
|
||||||
-v /opt/docker/projman-m2:/root/.m2 \
|
-v /opt/docker/projman-m2:/root/.m2 \
|
||||||
-w /build maven:3.6.3-jdk-8 sleep 3600
|
-w /build maven:3.6.3-jdk-8 sleep 3600
|
||||||
docker cp "${PWD}/." mavenbuild:/build/
|
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
|
||||||
docker exec mavenbuild sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
|
docker exec "$BUILD_CONTAINER" sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
|
||||||
docker exec mavenbuild sh -c 'cd /build && mvn -B -DskipTests clean package'
|
docker exec "$BUILD_CONTAINER" sh -c 'cd /build && mvn -B -DskipTests clean package'
|
||||||
docker exec mavenbuild sh -c 'cd /build/target && cp *.jar app.jar'
|
docker exec "$BUILD_CONTAINER" sh -c 'cd /build/target && cp *.jar app.jar'
|
||||||
mkdir -p "${PWD}/target"
|
mkdir -p "${PWD}/target"
|
||||||
docker cp mavenbuild:/build/target/app.jar "${PWD}/target/app.jar"
|
docker cp "$BUILD_CONTAINER":/build/target/app.jar "${PWD}/target/app.jar"
|
||||||
docker rm -f mavenbuild
|
docker rm -f "$BUILD_CONTAINER"
|
||||||
|
|
||||||
- name: 构建并推送镜像
|
- name: 构建并推送镜像
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Notice 生产配置(线上以 nacos 的 notice.yaml 为准,本文件是 nacos 不可达时的兜底)
|
||||||
|
# 真正上线请在 Nacos 新建 dataId=notice.yaml、group=NOTICE 的配置覆盖以下项
|
||||||
|
# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
|
||||||
|
server:
|
||||||
|
port: 8070
|
||||||
|
servlet:
|
||||||
|
# 对外经 caddy 暴露为 http://base.yunhaixingtong.com/notice
|
||||||
|
context-path: /notice
|
||||||
|
|
||||||
|
spring:
|
||||||
|
mvc:
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
jackson:
|
||||||
|
time-zone: GMT+8
|
||||||
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
datasource:
|
||||||
|
# 与 UserBase 共用同一台 mysql8;库名为 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/notice
|
||||||
|
allow-headers: all
|
||||||
|
allow-origins: all
|
||||||
|
url-pattern: /api/*
|
||||||
|
app-auth:
|
||||||
|
enabled: true
|
||||||
|
# 本应用自身身份(客户端签名用)
|
||||||
|
app-key: 100617
|
||||||
|
app-secret: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
|
||||||
|
# 受信应用(服务端验签用):appKey -> appSecret
|
||||||
|
trusted-apps:
|
||||||
|
100617: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
|
||||||
|
|
||||||
|
# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data
|
||||||
|
# 键名必须是 file.upload-path(Java 端 @Value 按此读取;旧 FILE_PATH 顶层键不生效)
|
||||||
|
file:
|
||||||
|
upload-path: /data/upload
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Notice 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml)
|
||||||
|
# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到
|
||||||
|
# 镜像内烤好的 application-prod.yml 兜底默认。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[run-app] starting Notice (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..."
|
||||||
|
exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/notice.jar \
|
||||||
|
--spring.profiles.active=prod
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<?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>notice</artifactId>
|
||||||
|
<version>0.0.1</version>
|
||||||
|
<name>Notice</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>
|
||||||
|
<!-- 外观偏好同源读端点 GET /api/appearance(三态 proxy/local/off,默认 proxy 经 YschOpenCall
|
||||||
|
代理 UserHome /oapi/appearance,userhome 地址走平台 application-platform.yml 兜底)。
|
||||||
|
平台件不进聚合包:本地同名控制器已删(2026-10-12 任务 E 收敛,starter 端点同 URL 同语义,
|
||||||
|
503 直写铁律已内置) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunhai.platform</groupId>
|
||||||
|
<artifactId>yh-appearance-spring-boot-starter</artifactId>
|
||||||
|
<!-- 显式版本(2026-10-17 过夜任务环境修复):该件由 BasePlatform 的 feat/appearance-sdk
|
||||||
|
分支引入并只在该分支的 yh-platform-dependencies 里托管;本次接入件工作基于
|
||||||
|
feat/flow-embed(BOM 无此条目),按 BOM 解析会缺版本导致整仓编译断。jar 本体已在本地仓库,
|
||||||
|
补版本号即可恢复。appearance 分支合流后应删掉本行回到 BOM 托管。 -->
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 流程中心回调接收端 starter(2026-09-20 迁移):验签外置(/oapi/* 平台验签),
|
||||||
|
接收管线(幂等/乱序门/同事务推进)+ 登记注册器组件化,业务映射见 NoticeFlowCallbackHandler -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunhai.platform</groupId>
|
||||||
|
<artifactId>yh-flow-callback-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tier 3 语义接入件迁移试点(2026-10-17 ysch-flow 最小闭环 T3):事件→状态声明化 +
|
||||||
|
发起编排模板 + 变量契约预检 + bizUrl 自动装配。它聚合传递上面 callback 件与 openapi 件,
|
||||||
|
本处保留 callback 显式声明(NoticeFlowCallbackHandler 作为回退绳仍在编译期需要其 SPI 类型)。
|
||||||
|
业务映射见 NoticeFlowBizAdapter,版本由 yh-platform-dependencies BOM 管理。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunhai.platform</groupId>
|
||||||
|
<artifactId>ysch-flow-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>
|
||||||
|
|
||||||
|
<!-- 入站验签走中央密钥链(注册表模式)必须显式引 data-redis:yh-app-auth 的该依赖是
|
||||||
|
optional、不传递。副作用是 RedisHealthIndicator 会被装配,故 yml 里关掉
|
||||||
|
management.health.redis.enabled,防 Redis 抖动把 /actuator/health 拉成 DOWN(503)。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>notice</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.notice.NoticeApplication</mainClass>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<!-- 本工程测试命名约定:纯单元 *Test + 集成 *IT(连本地 MySQL),统一由 surefire 执行
|
||||||
|
(surefire 默认只匹配 *Test.java,不追加则 *IT 永不执行) -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<includes>
|
||||||
|
<include>**/*Test.java</include>
|
||||||
|
<include>**/*IT.java</include>
|
||||||
|
</includes>
|
||||||
|
</configuration>
|
||||||
|
</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,20 @@
|
|||||||
|
package com.yunhai.notice;
|
||||||
|
|
||||||
|
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.notice")
|
||||||
|
@EnableTransactionManagement
|
||||||
|
@EnableScheduling
|
||||||
|
public class NoticeApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 开启 fastjson 安全模式
|
||||||
|
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||||
|
SpringApplication.run(NoticeApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.yunhai.notice.common.interceptor;
|
||||||
|
|
||||||
|
import com.yunhai.platform.auth.BoaUserContext;
|
||||||
|
import com.yunhai.platform.auth.UserContextHolder;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端 /mobiapi 访问拦截(口径 = 登录即可访问)。
|
||||||
|
* <p>不做访问级别判级——移动端主体含学生(旧 SUPER/BUREAU/SCHOOL/NONE 判级照抄会全 403)。
|
||||||
|
* token 解析、401+40101 信封与 pre-token 豁免由平台 SDK {@code LoginUserInterceptor} 承担
|
||||||
|
* (tokenSurfaces 缺省 /mobiapi);本拦截器是业务侧兜底:SDK 之后的任何 /mobiapi 业务请求,
|
||||||
|
* 身份上下文为空即 401,防止依赖链路变化时出现"匿名可达的业务端点"。</p>
|
||||||
|
*/
|
||||||
|
public class MobiAccessInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||||
|
if (UserContextHolder.get() == null && BoaUserContext.get() == null) {
|
||||||
|
try {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write(
|
||||||
|
"{\"code\":40101,\"msg\":\"未登录或登录已过期\",\"data\":null}");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 响应已提交等场景:状态码已设置,写体失败不掩盖原始语义
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.yunhai.notice.common.listener;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.common.validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||||
|
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||||
|
*/
|
||||||
|
public interface ValidationGroups {
|
||||||
|
interface Create {}
|
||||||
|
interface Update {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.yunhai.notice.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局路径常量(三层接口面,闭集):/api PC 管理端(CAS)、/mobiapi 移动端(token)、/oapi 开放接口(应用签名)。
|
||||||
|
* 其余前缀(如旧 /manage、/{platform} 通配)一律 404(方案 §4.1 路径闭集)。
|
||||||
|
*/
|
||||||
|
public enum Config {
|
||||||
|
Inst;
|
||||||
|
|
||||||
|
// 末尾不带斜杠:各控制器用 API_PATH + "/xxx/" 拼路径,若这里带尾斜杠会拼出字面双斜杠而 404
|
||||||
|
public static final String API_PATH = "/api"; // PC 管理端(CAS 会话)
|
||||||
|
public static final String MOBI_API_PATH = "/mobiapi"; // 移动端(平台 Bearer token)
|
||||||
|
public static final String OPEN_API_PATH = "/oapi";
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.yunhai.notice.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,27 @@
|
|||||||
|
package com.yunhai.notice.config;
|
||||||
|
|
||||||
|
import com.yunhai.notice.common.interceptor.MobiAccessInterceptor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端 /mobiapi 拦截装配。
|
||||||
|
* <p>{@code LOWEST_PRECEDENCE} 保证本配置器在 SDK 的 {@code YhAuthWebMvcConfigurer}
|
||||||
|
* (无 @Order)之后注册,登录态解析先于本兜底拦截执行。</p>
|
||||||
|
* <p>排除 {@code /mobiapi/auth/**}:其中 login/bind/exchange 是 SDK 定义的 pre-token 端点
|
||||||
|
* ({@code LoginUserInterceptor.PRE_TOKEN_ENDPOINTS}),设计上匿名可达——它们是登录链本身,
|
||||||
|
* 不是"业务免登录端点";logout/jsapiSignature 仍由 SDK 按 token 强制。业务端点无任何白名单。</p>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class MobiWebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new MobiAccessInterceptor())
|
||||||
|
.addPathPatterns("/mobiapi/**")
|
||||||
|
.excludePathPatterns("/mobiapi/auth/**")
|
||||||
|
.order(Ordered.LOWEST_PRECEDENCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.yunhai.notice.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,111 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.notice.service.IFileService;
|
||||||
|
import com.yunhai.notice.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 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("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.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.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,81 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开通短信(前端 getSchModuleIsSms,迁移自 base/GetSchModuleIsSms)。
|
||||||
|
*/
|
||||||
|
@GetMapping("Sync")
|
||||||
|
public Boolean sync() {
|
||||||
|
return dictService.getSchModuleIsSms();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.notice.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,182 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.notice.service.flow.NoticeApprovalClient;
|
||||||
|
import com.yunhai.notice.service.flow.NoticeFlowEmbedService;
|
||||||
|
import com.yunhai.notice.service.flow.NoticeFlowGuard;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内审批(第三步 T-C4):"我的待审核"数据面 + v2.1 业务详情卡片/记录/完整办理换址。
|
||||||
|
* 前端不直调 oapi——operatorId/operatorName 一律取 CAS 登录态(BoaUser),请求参数里不收身份字段,
|
||||||
|
* 防伪造办理人;对端 flow-center 再按任务办理人复核(双层)。
|
||||||
|
* <ul>
|
||||||
|
* <li>GET Approval/Todo:待办分页(bizType=notice 固定);</li>
|
||||||
|
* <li>GET Approval/Instances:办件分页(status 可选);</li>
|
||||||
|
* <li>GET Approval/ByBiz:当前人在该通知上的待办(0/1/N,卡片据此决定可写还是只读);</li>
|
||||||
|
* <li>GET Approval/RecordUrl:审批记录抽屉地址(token 只读页,复用 {@link NoticeFlowEmbedService});</li>
|
||||||
|
* <li>GET Approval/TaskPageUrl:完整办理任务页地址(先确认 taskId 属于当前人待办);</li>
|
||||||
|
* <li>POST Approval/Approve / Reject:办理(body 仅 opinion/edgeName)。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/notice/approval/")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeApprovalController {
|
||||||
|
|
||||||
|
private final NoticeApprovalClient approvalClient;
|
||||||
|
private final NoticeFlowEmbedService embedService;
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
private final NoticeFlowGuard noticeFlowGuard;
|
||||||
|
|
||||||
|
/** 我的待审核(列表页数据源) */
|
||||||
|
@GetMapping("Todo")
|
||||||
|
public Map<String, Object> todo(BoaUser user,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "size", required = false) Integer size) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
return approvalClient.todoTasks(op.schId, op.userId, page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 办件视角(对账/历史;status 10 审批中/20 完成/30 拒绝/40 撤回) */
|
||||||
|
@GetMapping("Instances")
|
||||||
|
public Map<String, Object> instances(BoaUser user,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
|
@RequestParam(value = "size", required = false) Integer size) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
return approvalClient.instances(op.schId, status, page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过(body: opinion 可选 / edgeName 多出口可选) */
|
||||||
|
@PostMapping("Approve")
|
||||||
|
public void approve(@RequestParam("taskId") Long taskId, BoaUser user,
|
||||||
|
@RequestBody(required = false) Map<String, String> body) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
String opinion = body == null ? null : body.get("opinion");
|
||||||
|
String edgeName = body == null ? null : body.get("edgeName");
|
||||||
|
approvalClient.approve(taskId, op.schId, op.userId, op.name, opinion, edgeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝(opinion 必填) */
|
||||||
|
@PostMapping("Reject")
|
||||||
|
public void reject(@RequestParam("taskId") Long taskId, BoaUser user,
|
||||||
|
@RequestBody(required = false) Map<String, String> body) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
approvalClient.reject(taskId, op.schId, op.userId, op.name,
|
||||||
|
body == null ? null : body.get("opinion"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前人在该通知上的待办(v2.1 卡片数据源)。身份只取会话;{@code serid} 先过与记录页
|
||||||
|
* 同一套可见性守卫(未发布通知仅提交人/管理员/办理人/被抄送人可见)。
|
||||||
|
* <p>出参 {@code tasks} 的 0/1/N 由前端据此决定"只读 / 可办理 / 多个待办引导去列表",
|
||||||
|
* 本端不猜选、不合并。办件不存在与"有办件但不是我的待办"同形(空 tasks),不用于探测。
|
||||||
|
*/
|
||||||
|
@GetMapping("ByBiz")
|
||||||
|
public Map<String, Object> byBiz(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
requireVisible(serid, user);
|
||||||
|
return approvalClient.byBiz(op.schId, op.userId, serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批记录抽屉地址(token 只读页):直接复用 {@link NoticeFlowEmbedService#embedUrl},
|
||||||
|
* 那里已做通知存在性 + 未发布可见性 + "尚未进入流程"三重校验,且 token 现取现发不缓存。
|
||||||
|
* 与 {@code /api/notice/flow/Embed} 同一条实现,避免两个入口对同一 Drawer 漂移出不同守卫口径。
|
||||||
|
*/
|
||||||
|
@GetMapping("RecordUrl")
|
||||||
|
public Map<String, String> recordUrl(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
requireOperator(user);
|
||||||
|
return embedService.embedUrl(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整办理任务页地址(CAS 会话页,无 token)。
|
||||||
|
* <p>先打 byBiz 确认 {@code taskId} 确实在<b>当前人</b>该通知的待办里才签发——任务页是写面,
|
||||||
|
* 顺手签发一个别人的 taskId 等于替他人挂一个办理入口。flow-center 侧仍会按 assigneeId
|
||||||
|
* 逐字复核,本处只是不把无效地址散播到前端。
|
||||||
|
*/
|
||||||
|
@GetMapping("TaskPageUrl")
|
||||||
|
public Map<String, Object> taskPageUrl(@RequestParam("serid") Integer serid,
|
||||||
|
@RequestParam("taskId") Long taskId, BoaUser user) {
|
||||||
|
Operator op = requireOperator(user);
|
||||||
|
requireVisible(serid, user);
|
||||||
|
if (!isMyPendingTask(serid, taskId, op)) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "该任务不是你在本通知上的待办");
|
||||||
|
}
|
||||||
|
return approvalClient.taskPageUrl(op.schId, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** byBiz 出参里是否含该 taskId(对端 tasks 元素为 {taskId,nodeId,nodeName,arriveTime,outlets}) */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private boolean isMyPendingTask(Integer serid, Long taskId, Operator op) {
|
||||||
|
Map<String, Object> data = approvalClient.byBiz(op.schId, op.userId, serid);
|
||||||
|
Object tasks = data == null ? null : data.get("tasks");
|
||||||
|
if (!(tasks instanceof List)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Object row : (List<Object>) tasks) {
|
||||||
|
if (row instanceof Map) {
|
||||||
|
Object id = ((Map<String, Object>) row).get("taskId");
|
||||||
|
if (id instanceof Number && taskId.equals(((Number) id).longValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知可见性守卫(口径与 {@link NoticeFlowEmbedService#embedUrl} 逐字一致):存在性 +
|
||||||
|
* 未发布(status!=1)走 {@link NoticeFlowGuard}。已发布通知照常放行(全校可见,审批数据面
|
||||||
|
* 只对办理人出 tasks)。
|
||||||
|
*/
|
||||||
|
private void requireVisible(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = noticeRepository.findById(noticeId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "通知不存在: " + noticeId));
|
||||||
|
if (notice.getStatus() == null || notice.getStatus() != 1) {
|
||||||
|
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录态提参:无身份字段可被请求注入;姓名兜底账号(与 NoticeFlowService 提交人口径一致) */
|
||||||
|
private static Operator requireOperator(BoaUser user) {
|
||||||
|
if (user == null || user.getId() == null) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "未登录");
|
||||||
|
}
|
||||||
|
if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "当前身份无学校归属");
|
||||||
|
}
|
||||||
|
String name = StringUtils.hasText(user.getName()) ? user.getName() : user.getAccount();
|
||||||
|
return new Operator(user.getId(), user.getCurrentRole().getSchId(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Operator {
|
||||||
|
final Integer userId;
|
||||||
|
final Integer schId;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
Operator(Integer userId, Integer schId, String name) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.schId = schId;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.entity.NoticeCc;
|
||||||
|
import com.yunhai.notice.enums.FlowAuditStatusEnum;
|
||||||
|
import com.yunhai.notice.repository.NoticeCcRepository;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内「抄送给我的」(2026-09-24):数据源是 flow.cc.sent 回调落的 notice_cc 表
|
||||||
|
* (NOTIFY 节点激活即抄送,审批过程中就可见——此时通知尚未发布,属知会不是可读)。
|
||||||
|
* 身份一律取 CAS 登录态(BoaUser),与 NoticeApprovalController 同口径,防伪造查询人。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/notice/cc/")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeCcController {
|
||||||
|
|
||||||
|
private final NoticeCcRepository noticeCcRepository;
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
|
||||||
|
/** 抄送给我的(分页,sent_time 倒序;行内回填通知标题/部门/审核状态供列表直渲染) */
|
||||||
|
@GetMapping("My")
|
||||||
|
public Map<String, Object> my(BoaUser user,
|
||||||
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "size", defaultValue = "10") Integer size) {
|
||||||
|
if (user == null || user.getId() == null) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "未登录");
|
||||||
|
}
|
||||||
|
if (user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "当前身份无学校归属");
|
||||||
|
}
|
||||||
|
int pageIndex = page == null || page < 1 ? 1 : page;
|
||||||
|
int pageSize = size == null || size < 1 ? 10 : Math.min(size, 100);
|
||||||
|
Page<NoticeCc> ccPage = noticeCcRepository.findBySchIdAndCcUserIdOrderBySentTimeDescIdDesc(
|
||||||
|
user.getCurrentRole().getSchId(), user.getId(), PageRequest.of(pageIndex - 1, pageSize));
|
||||||
|
|
||||||
|
// 批量回填通知信息(被删通知保留抄送行,标题置空由前端兜底)
|
||||||
|
Map<Integer, Notice> notices = ccPage.getContent().isEmpty()
|
||||||
|
? new HashMap<>()
|
||||||
|
: noticeRepository.findAllById(ccPage.getContent().stream()
|
||||||
|
.map(NoticeCc::getNoticeId).collect(Collectors.toSet())).stream()
|
||||||
|
.collect(Collectors.toMap(Notice::getId, Function.identity()));
|
||||||
|
List<Map<String, Object>> content = new ArrayList<>();
|
||||||
|
for (NoticeCc cc : ccPage.getContent()) {
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
row.put("id", cc.getId());
|
||||||
|
row.put("noticeId", cc.getNoticeId());
|
||||||
|
row.put("flowInstanceId", cc.getFlowInstanceId());
|
||||||
|
row.put("nodeId", cc.getNodeId());
|
||||||
|
row.put("nodeName", cc.getNodeName());
|
||||||
|
row.put("sentTime", cc.getSentTime());
|
||||||
|
Notice notice = notices.get(cc.getNoticeId());
|
||||||
|
if (notice != null) {
|
||||||
|
row.put("title", notice.getTitle());
|
||||||
|
row.put("deptName", notice.getDeptName());
|
||||||
|
row.put("publisherName", notice.getPublisherName());
|
||||||
|
row.put("noticeStatus", notice.getStatus());
|
||||||
|
row.put("auditStatus", notice.getAuditStatus());
|
||||||
|
row.put("auditStatusDesc", FlowAuditStatusEnum.descOf(notice.getAuditStatus()));
|
||||||
|
} else {
|
||||||
|
row.put("title", null);
|
||||||
|
row.put("noticeStatus", null);
|
||||||
|
row.put("auditStatus", null);
|
||||||
|
row.put("auditStatusDesc", "通知已删除");
|
||||||
|
}
|
||||||
|
content.add(row);
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("total", ccPage.getTotalElements());
|
||||||
|
result.put("content", content);
|
||||||
|
result.put("pageIndex", pageIndex);
|
||||||
|
result.put("pageSize", pageSize);
|
||||||
|
result.put("totalPages", ccPage.getTotalPages());
|
||||||
|
result.put("hasNext", ccPage.hasNext());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.common.validation.ValidationGroups;
|
||||||
|
import com.yunhai.notice.model.BasePageResult;
|
||||||
|
import com.yunhai.notice.model.notice.*;
|
||||||
|
import com.yunhai.notice.service.INoticeService;
|
||||||
|
import com.yunhai.notice.utils.PageUtils;
|
||||||
|
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.servlet.http.HttpServletResponse;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
import com.yunhai.notice.service.flow.NoticeFlowService;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知-流程中心接缝接口(先审后发)。
|
||||||
|
* <ul>
|
||||||
|
* <li>GET Flow/Options:编辑页"审核流程"选择器(代理 FlowClient.listFlows,前端不直调 oapi);</li>
|
||||||
|
* <li>GET Flow/Binding:应用设置 > 流程绑定 的读取(登录可读;发通知页据此判断是否走审核);</li>
|
||||||
|
* <li>POST Flow/Binding/Save:保存/解除应用级绑定(仅本校管理员);</li>
|
||||||
|
* <li>POST Flow/TrySubmit:提交/重提审核(幂等,bizId=notice.id;apply 失败保持待提交可重推);</li>
|
||||||
|
* <li>GET Flow/Trace:审批进度(后端 getInstanceByBiz 组装,前端不直调 oapi——放行判定必须收在后端)。</li>
|
||||||
|
* <li>GET Flow/Embed:办件进度嵌入页地址(Drawer iframe 直嵌,可见性与 Trace 同口径)。</li>
|
||||||
|
* <li>GET Flow/DiagramDef:流程定义示意图(绑定页/发通知页 Drawer,登录即可读——与 Binding 同口径)。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.API_PATH + "/notice/flow/")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeFlowController {
|
||||||
|
|
||||||
|
private final NoticeFlowService noticeFlowService;
|
||||||
|
private final com.yunhai.notice.service.flow.NoticeFlowEmbedService noticeFlowEmbedService;
|
||||||
|
private final com.yunhai.notice.service.flow.FlowClient flowClient;
|
||||||
|
|
||||||
|
/** 审核流程下拉选项(流程中心未启用时返回空列表,前端隐藏选择器) */
|
||||||
|
@GetMapping("Options")
|
||||||
|
public List<FlowModels.FlowSummaryVo> options(BoaUser user,
|
||||||
|
@RequestParam(value = "schId", required = false) Integer schId) {
|
||||||
|
return noticeFlowService.options(schId != null ? schId : currentSchId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用设置 > 流程绑定:本校绑定视图(bound/flowGuid/flowName/flowEnabled)。
|
||||||
|
* 登录即可读——发通知页靠它决定「提交审核」还是「发送」。
|
||||||
|
*/
|
||||||
|
@GetMapping("Binding")
|
||||||
|
public Map<String, Object> binding(BoaUser user) {
|
||||||
|
return noticeFlowService.bindingView(currentSchId(user), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存/解除绑定(flowGuid 空=解绑);仅本校管理员可改 */
|
||||||
|
@PostMapping("Binding/Save")
|
||||||
|
public Map<String, Object> saveBinding(@RequestParam(value = "flowGuid", required = false) String flowGuid,
|
||||||
|
BoaUser user) {
|
||||||
|
return noticeFlowService.saveBinding(user, flowGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交/重新提交审核(编辑页重提按钮;audit=10 审批中将被拒绝;BP-R5-3 补属主守卫) */
|
||||||
|
@PostMapping("TrySubmit")
|
||||||
|
public void trySubmit(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
noticeFlowService.trySubmitFlow(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批进度(详情页进度卡片;未绑流程返回 bound=false 的未接入语义;未发布仅三类人可见) */
|
||||||
|
@GetMapping("Trace")
|
||||||
|
public Map<String, Object> trace(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
return noticeFlowService.buildTrace(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回审核(2026-09-17 治理:原先前端直调 flow-center 用户面,现后端经 SDK 托管件中转;
|
||||||
|
* 仅创建人本人 + 审批中,撤回后状态由 flow.withdrawn 回调推进)。
|
||||||
|
*/
|
||||||
|
@PostMapping("Withdraw")
|
||||||
|
public void withdraw(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
noticeFlowService.withdrawNotice(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程示意图(PNG 流,同源直嵌 img;可见性与 Trace 同口径,token 不落前端) */
|
||||||
|
@GetMapping(value = "Diagram", produces = org.springframework.http.MediaType.IMAGE_PNG_VALUE)
|
||||||
|
public byte[] diagram(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
return noticeFlowService.diagramImage(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义示意图(PNG 流,绑定页/发通知页 Drawer 同源直嵌 img)。def 口径无办件,
|
||||||
|
* 不适用 NoticeFlowGuard 办件守卫——与绑定读取(Binding,登录即可读)同口径:CAS 登录即可,
|
||||||
|
* schId 从登录用户推导,本校限定由 flow-center 按 schId+flowGuid 收敛;
|
||||||
|
* 绑定已解除/无已发布版时对端报业务错(JSON 信封),前端 img onerror 转提示,不留 500。
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "DiagramDef", produces = org.springframework.http.MediaType.IMAGE_PNG_VALUE)
|
||||||
|
public byte[] diagramDef(@RequestParam("flowGuid") String flowGuid, BoaUser user) {
|
||||||
|
if (flowGuid == null || flowGuid.trim().isEmpty()) {
|
||||||
|
throw new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "缺少流程标识");
|
||||||
|
}
|
||||||
|
return flowClient.diagramImageByFlowGuid(currentSchId(user), flowGuid.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 办件进度嵌入页地址(详情页 Drawer 内 iframe 直嵌 flow-center 统一进度页)。
|
||||||
|
* 可见性与 Trace/Diagram 同口径;pageUrl 含短时 token,现取现发不落前端缓存。
|
||||||
|
*/
|
||||||
|
@GetMapping("Embed")
|
||||||
|
public Map<String, String> embed(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||||
|
return noticeFlowEmbedService.embedUrl(serid, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer currentSchId(BoaUser user) {
|
||||||
|
if (user == null || user.getCurrentRole() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user.getCurrentRole().getSchId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.yunhai.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.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.notice.controller.api;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.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,85 @@
|
|||||||
|
package com.yunhai.notice.controller.mobiapi;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.notice.service.IFileService;
|
||||||
|
import com.yunhai.notice.service.IUserCenterService;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
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;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台代理域移动端接口(/mobiapi/base/,token 身份)。
|
||||||
|
* <p>端点从 {@code controller.api.BaseApiController} 按移动端需要迁出,业务逻辑仍在
|
||||||
|
* {@link IUserCenterService} / {@link IFileService},本控制器仅"取参 → 调 Service → 返回";
|
||||||
|
* 管理端专用端点留在 api/ 面,本期不删。</p>
|
||||||
|
* <p>出入参口径与 api 侧一致(路径名沿用旧移动端 base/FilePreview|FileDown 调用形状;
|
||||||
|
* 文件预览/下载在 api 侧实际位于 FileController 的 file/Down|Preview,此处对齐其
|
||||||
|
* IFileService 签名)。登出不在此暴露——SDK 已提供 {@code POST /mobiapi/auth/logout}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.MOBI_API_PATH + "/base/")
|
||||||
|
public class BaseMobiController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IUserCenterService userCenterService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFileService fileService;
|
||||||
|
|
||||||
|
/** 获取当前登录用户信息(token 链注入)。 */
|
||||||
|
@GetMapping("GetUserInfo")
|
||||||
|
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||||
|
return authUserWithDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前用户已授权的模块列表(移动端仅用于发布权限判定)。 */
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前用户学校组及用户(树形结构)。 */
|
||||||
|
@GetMapping("GetSchoolAll")
|
||||||
|
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
|
||||||
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
return userCenterService.getSchoolAllTree(schId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定组内用户(教师 + 管理员)。 */
|
||||||
|
@GetMapping("GetGroupUser")
|
||||||
|
public List<Map<String, Object>> getGroupUser(
|
||||||
|
@RequestParam("gid") Integer gid,
|
||||||
|
@RequestParam(value = "role", defaultValue = "2") Integer role) {
|
||||||
|
return userCenterService.getGroupUser(gid, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件上传(单文件)。返回 { fileid, host, fileurl, name, type, size }。 */
|
||||||
|
@PostMapping("FileUpload")
|
||||||
|
public UpFileResultVo fileUpload(@RequestParam("file") MultipartFile file) {
|
||||||
|
return fileService.uploadToResult(file, "notice");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件在线预览(inline 输出,前端取 Blob 渲染;type 兼容旧调用参数)。 */
|
||||||
|
@GetMapping("FilePreview")
|
||||||
|
public void filePreview(@RequestParam("id") Integer id,
|
||||||
|
@RequestParam(value = "type", required = false) Integer type,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
fileService.preview(id, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 附件下载。 */
|
||||||
|
@GetMapping("FileDown")
|
||||||
|
public void fileDown(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||||
|
fileService.download(id, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.yunhai.notice.controller.mobiapi;
|
||||||
|
|
||||||
|
import com.yunhai.notice.config.Config;
|
||||||
|
import com.yunhai.notice.model.BasePageResult;
|
||||||
|
import com.yunhai.notice.model.notice.*;
|
||||||
|
import com.yunhai.notice.service.INoticeService;
|
||||||
|
import com.yunhai.notice.utils.PageUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.SortDefault;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知公告移动端接口(/mobiapi 面,token 身份)。
|
||||||
|
* <p>路径前缀:{@code Config.MOBI_API_PATH + "/notice/"};方法名/参数与
|
||||||
|
* {@code controller.api.NoticeController} 保持一致,注入同一 {@link INoticeService},
|
||||||
|
* 不改 Service 实现;返回裸对象,由 ApiResponseAdvice 统一包装 {code, msg, data}。</p>
|
||||||
|
* <p>口径:登录即可访问(移动端主体含学生),不按管理端访问级别判级;
|
||||||
|
* 拦截与 401 语义见 {@code config.MobiWebMvcConfig} / {@code common.interceptor.MobiAccessInterceptor}。</p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = Config.MOBI_API_PATH + "/notice/")
|
||||||
|
public class NoticeMobiController {
|
||||||
|
|
||||||
|
@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("ReadDetail")
|
||||||
|
public List<Map<String, Object>> readDetail(@RequestParam("serid") Integer serid) {
|
||||||
|
return noticeService.getReadDetailGrouped(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 催收未读。 */
|
||||||
|
@PostMapping("Expedite")
|
||||||
|
public void expedite(@RequestParam("serid") Integer serid) {
|
||||||
|
noticeService.expedite(serid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一键全部已读。 */
|
||||||
|
@PostMapping("InitializeRed")
|
||||||
|
public InitializeRedVo initializeRed() {
|
||||||
|
return noticeService.initializeRedResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上一篇/下一篇通知ID({prevId, nextId},0=无)。 */
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yunhai.notice.controller.oapi;
|
||||||
|
|
||||||
|
import com.yunhai.notice.service.IDictService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开放接口 demo(两级模型试点 P2.1:Notice 作为提供方开放 /oapi/**)。
|
||||||
|
* 强制验签由 starter 提供(yml {@code yh.app-auth.server.enforce-patterns: [/oapi/*]}):
|
||||||
|
* 浏览器/匿名直访 401,注册应用带 X-App-* 签名三件套直调通过(trusted-apps 收调用方密钥)。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/oapi/notice")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OapiDictController {
|
||||||
|
|
||||||
|
private final IDictService dictService;
|
||||||
|
|
||||||
|
/** 部门字典(demo:type=1 部门;调用方签名鉴权由 enforce-patterns 强制) */
|
||||||
|
@GetMapping("/getDict")
|
||||||
|
public List<Map<String, Object>> getDict(@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||||
|
return dictService.getSysDic(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.yunhai.notice.controller.oapi;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件路由判断端点(2026-09-24 设计方案 §5.2):flow-center CONDITION 节点(routerKey=
|
||||||
|
* notice.dept-judge)激活时同步调用——发文部门=校长室走"校长室"线(免校长审核),其余走"其他部门"线。
|
||||||
|
* <ul>
|
||||||
|
* <li>验签:收在 /oapi/* 下由 AppAuthFilter 强制验 flow-center(100019)的应用签名,零新概念;</li>
|
||||||
|
* <li>纯读幂等:凭 bizId 查 notice 表比对发文部门(variables 里有 formData 的部门则免查库,两口径一致);
|
||||||
|
* <li>契约纪律:返回 outlet 必须命中 outlets——流程定义与端点的出口名是同一份契约,不命中抛业务错
|
||||||
|
* (flow-center 侧按判断失败挂起,绝不默认放行);</li>
|
||||||
|
* <li>轻实现:同步 3s 超时,只做一次主键查询。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/oapi/notice/flow/route")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OapiFlowRouteController {
|
||||||
|
|
||||||
|
private static final String OUTLET_PRINCIPAL = "校长室";
|
||||||
|
private static final String OUTLET_OTHER = "其他部门";
|
||||||
|
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
|
||||||
|
/** 判断请求体(契约见《子应用接入流程中心规范.md》条件路由判断接口;字段忽略未知向前兼容) */
|
||||||
|
@Data
|
||||||
|
public static class JudgeBody {
|
||||||
|
private Long schId;
|
||||||
|
private Long instanceId;
|
||||||
|
private Integer instanceVersion;
|
||||||
|
private String bizType;
|
||||||
|
private String bizId;
|
||||||
|
private String nodeId;
|
||||||
|
private String nodeName;
|
||||||
|
private String routerKey;
|
||||||
|
private List<String> outlets;
|
||||||
|
private Map<String, Object> variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dept-judge")
|
||||||
|
public Map<String, Object> deptJudge(@RequestBody JudgeBody body) {
|
||||||
|
Integer noticeId;
|
||||||
|
try {
|
||||||
|
noticeId = Integer.valueOf(String.valueOf(body.getBizId()).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "bizId 非法:" + body.getBizId());
|
||||||
|
}
|
||||||
|
// formData 优先(apply 时透传了发文部门则免查库),缺失回落查表——两口径同一数据源
|
||||||
|
String deptName = variablesDeptName(body);
|
||||||
|
String deptId = null;
|
||||||
|
if (!StringUtils.hasText(deptName)) {
|
||||||
|
Notice notice = noticeRepository.findById(noticeId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "通知不存在:" + noticeId));
|
||||||
|
deptId = notice.getDeptId();
|
||||||
|
deptName = notice.getDeptName();
|
||||||
|
}
|
||||||
|
String outlet = OUTLET_PRINCIPAL.equals(deptName) ? OUTLET_PRINCIPAL : OUTLET_OTHER;
|
||||||
|
List<String> outlets = body.getOutlets() == null ? Collections.emptyList() : body.getOutlets();
|
||||||
|
if (!outlets.contains(outlet)) {
|
||||||
|
// 出口契约不一致(流程定义改了出口名而端点没跟上):抛错让 flow-center 挂起,不默认放行
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"判断结果未命中出口集合:outlet=" + outlet + " outlets=" + outlets
|
||||||
|
+ "(deptId=" + deptId + ", deptName=" + deptName + ")");
|
||||||
|
}
|
||||||
|
return Collections.singletonMap("outlet", outlet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** formData 里的发文部门(apply 时 bizSnapshot 同源透传;缺字段返回空串回落查表) */
|
||||||
|
private static String variablesDeptName(JudgeBody body) {
|
||||||
|
Map<String, Object> variables = body.getVariables();
|
||||||
|
if (variables == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Object v = variables.get("deptName");
|
||||||
|
return v == null ? "" : String.valueOf(v).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.yunhai.notice.entity;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.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.notice.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,178 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:1 已发布(无草稿态,见 {@link com.yunhai.notice.enums.NoticeStatusEnum})
|
||||||
|
*/
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
private Integer status = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否置顶: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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信实际发送时间(防重复发送的幂等标记):NULL=未发。
|
||||||
|
* 直发/审批通过/到点补发三条链路共用(SmsSendService 原子认领);本地随 ddl-auto=update
|
||||||
|
* 自动加列,生产执行 deploy/sql/20260913_add_sms_sent_at.sql。
|
||||||
|
*/
|
||||||
|
@Column(name = "sms_sent_at")
|
||||||
|
private LocalDateTime smsSentAt;
|
||||||
|
|
||||||
|
// ===== 先审后发(流程中心托管):全部可空,NULL=未绑流程(原直发链路,存量零迁移) =====
|
||||||
|
|
||||||
|
/** 绑定的流程 GUID;NULL=直发(原链路一字不改) */
|
||||||
|
@Column(name = "flow_guid", length = 32)
|
||||||
|
private String flowGuid;
|
||||||
|
|
||||||
|
/** 流程中心办件 id(apply 成功回填) */
|
||||||
|
@Column(name = "flow_instance_id")
|
||||||
|
private Long flowInstanceId;
|
||||||
|
|
||||||
|
/** 审核镜像状态:0 待提交 / 10 审批中 / 20 通过 / 30 驳回 / 40 已撤回(FlowAuditStatusEnum) */
|
||||||
|
@Column(name = "audit_status")
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
/** 已消费的最大回调 instanceVersion(乱序丢弃闸门) */
|
||||||
|
@Column(name = "last_instance_version")
|
||||||
|
private Integer lastInstanceVersion;
|
||||||
|
|
||||||
|
/** 终态明细快照(result/opinion/rejectNode,详情页进度卡片展示) */
|
||||||
|
@Column(name = "flow_result_json", columnDefinition = "LONGTEXT")
|
||||||
|
private String flowResultJson;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程抄送留痕(2026-09-24):flow-center NOTIFY 节点的 flow.cc.sent 回调逐人一行,
|
||||||
|
* 应用内「抄送给我的」列表数据源。幂等靠 starter 的 flow_callback_event(uk event_id),
|
||||||
|
* 本表只增不改——同一通知多节点抄送/退回重走重复抄送各成一行,轨迹按时间归位。
|
||||||
|
*/
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "notice_cc",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_notice_cc_user", columnList = "sch_id, cc_user_id, sent_time"),
|
||||||
|
@Index(name = "idx_notice_cc_notice", columnList = "notice_id")})
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||||
|
public class NoticeCc extends BaseEntity {
|
||||||
|
|
||||||
|
/** 学校ID(多租户隔离,与 notice.sch_id 同域) */
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
|
||||||
|
/** 关联通知(信封 bizId) */
|
||||||
|
@Column(name = "notice_id", nullable = false)
|
||||||
|
private Integer noticeId;
|
||||||
|
|
||||||
|
/** flow-center 办件 id(信封 instanceId) */
|
||||||
|
@Column(name = "flow_instance_id")
|
||||||
|
private Long flowInstanceId;
|
||||||
|
|
||||||
|
/** 抄送发生节点 id(data.nodeId) */
|
||||||
|
@Column(name = "node_id", length = 64)
|
||||||
|
private String nodeId;
|
||||||
|
|
||||||
|
/** 抄送环节名快照(data.nodeName) */
|
||||||
|
@Column(name = "node_name", length = 128)
|
||||||
|
private String nodeName;
|
||||||
|
|
||||||
|
/** 被抄送人用户 id(data.ccUserId,平台统一用户目录) */
|
||||||
|
@Column(name = "cc_user_id", nullable = false)
|
||||||
|
private Integer ccUserId;
|
||||||
|
|
||||||
|
/** 被抄送人姓名快照(data.ccUserName) */
|
||||||
|
@Column(name = "cc_user_name", length = 64)
|
||||||
|
private String ccUserName;
|
||||||
|
|
||||||
|
/** 抄送时间(事件处理时刻)。DATETIME(3) 与 flow-center flw_cc.sent_time 同口径 */
|
||||||
|
@Column(name = "sent_time", nullable = false, columnDefinition = "DATETIME(3)")
|
||||||
|
private LocalDateTime sentTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.yunhai.notice.entity;
|
||||||
|
|
||||||
|
import com.yunhai.notice.common.listener.NoticeAuditListener;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用级审核流程绑定(应用设置 > 流程绑定,按学校一条)。
|
||||||
|
* <p>
|
||||||
|
* 语义(先审后发规则单一事实源):「通知对象=全部用户」且本校已绑定流程时,发通知必须走审核
|
||||||
|
* (status=0 审核载体,回调 completed 才发布);「通知对象=指定人员」与未绑定一律直发。
|
||||||
|
* 行不存在 = 未绑定。通知落库时把 flow_guid 快照进 notice 行,故改绑/解绑不影响在途审批。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "notice_flow_binding",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_notice_flow_binding_sch", columnNames = {"sch_id"}))
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(NoticeAuditListener.class)
|
||||||
|
public class NoticeFlowBinding extends BaseEntity {
|
||||||
|
|
||||||
|
/** 租户键(一校一条绑定) */
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
|
||||||
|
/** 绑定的流程 GUID(flow-center flw_def.flow_guid,跨版本稳定) */
|
||||||
|
@Column(name = "flow_guid", length = 32)
|
||||||
|
private String flowGuid;
|
||||||
|
|
||||||
|
/** 流程名快照(流程中心不可达时设置页/提示条仍可显示) */
|
||||||
|
@Column(name = "flow_name", length = 128)
|
||||||
|
private String flowName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.yunhai.notice.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import com.yunhai.notice.common.listener.NoticeAuditListener;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.Index;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程中心回调事件去重表(照 svc-hall ServiceFlowEvent / flow-center 事件 uk 同构):
|
||||||
|
* eventId 唯一约束兜底并发;重复投递(flow-center 持续重试语义下必然发生)命中唯一键即跳过。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "notice_flow_event",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_notice_flow_event", columnNames = "event_id"),
|
||||||
|
indexes = @Index(name = "idx_notice_flow_event_inst", columnList = "instance_id"))
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(NoticeAuditListener.class)
|
||||||
|
public class NoticeFlowEvent extends BaseEntity {
|
||||||
|
|
||||||
|
/** 事件唯一键(32 位 uuid,flow-center 重试不变) */
|
||||||
|
@Column(name = "event_id", length = 40, nullable = false)
|
||||||
|
private String eventId;
|
||||||
|
|
||||||
|
/** 事件码:flow.submitted / task.created / task.handled / completed / rejected / withdrawn / ping */
|
||||||
|
@Column(name = "event", length = 32)
|
||||||
|
private String event;
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
|
||||||
|
@Column(name = "instance_id")
|
||||||
|
private Long instanceId;
|
||||||
|
|
||||||
|
@Column(name = "receive_time")
|
||||||
|
private java.time.LocalDateTime receiveTime;
|
||||||
|
|
||||||
|
@Column(name = "payload_json", columnDefinition = "LONGTEXT")
|
||||||
|
private String payloadJson;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.yunhai.notice.entity;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import javax.persistence.UniqueConstraint;
|
||||||
|
import com.yunhai.notice.common.listener.NoticeAuditListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程中心回调注册(照 svc-hall ServiceFlowReg / flow-center FlwCallbackReg 同构):
|
||||||
|
* sch+appKey+flowGuid 唯一;secret 为 flow-center reg 接口下发时落库的回调验签密钥。
|
||||||
|
* 注意:flow-center 重复注册会<b>轮换 secret</b>,故仅在缺失时注册(ensureRegistered),
|
||||||
|
* 绝不重复调用——失配恢复手段=管理端重新保存绑定。
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "notice_flow_reg",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_notice_flow_reg", columnNames = {"sch_id", "app_key", "flow_guid"}))
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(NoticeAuditListener.class)
|
||||||
|
public class NoticeFlowReg extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "sch_id")
|
||||||
|
private Integer schId;
|
||||||
|
|
||||||
|
/** 本应用在流程中心侧的 appKey(yh.app-auth.app-key,出站签名同一身份) */
|
||||||
|
@Column(name = "app_key", length = 64)
|
||||||
|
private String appKey;
|
||||||
|
|
||||||
|
@Column(name = "flow_guid", length = 32)
|
||||||
|
private String flowGuid;
|
||||||
|
|
||||||
|
@Column(name = "callback_url", length = 500)
|
||||||
|
private String callbackUrl;
|
||||||
|
|
||||||
|
/** 订阅事件逗号分隔,空=全部 */
|
||||||
|
@Column(name = "event_types", length = 200)
|
||||||
|
private String eventTypes;
|
||||||
|
|
||||||
|
/** flow-center reg 接口下发的回调验签密钥 */
|
||||||
|
@Column(name = "secret", length = 64)
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
/** 0 启用 / 1 停用(预留) */
|
||||||
|
@Column(name = "status")
|
||||||
|
private Integer status = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.yunhai.notice.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知接收人关联表(替代旧 receive_list JSON 字符串存储)。
|
||||||
|
* <p>
|
||||||
|
* 每条记录代表一个通知的一个接收人。全员通知(receive_list="ALL")不写明细行,
|
||||||
|
* 读取时按 receive_list="ALL" 标识走旧逻辑兜底。
|
||||||
|
*/
|
||||||
|
@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 {
|
||||||
|
|
||||||
|
/** 通知ID(关联 notice.id) */
|
||||||
|
@Column(name = "notice_id", nullable = false)
|
||||||
|
private Integer noticeId;
|
||||||
|
|
||||||
|
/** 接收人登录账号 */
|
||||||
|
@Column(name = "account", nullable = false, length = 64)
|
||||||
|
private String account;
|
||||||
|
|
||||||
|
/** 接收类型:1 接收 / 2 抄送 */
|
||||||
|
@Column(name = "receive_type", nullable = false)
|
||||||
|
private Integer receiveType = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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.notice.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.notice.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.notice.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.notice.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,42 @@
|
|||||||
|
package com.yunhai.notice.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知审核状态(audit_status 列,对齐 flow-center FlwEnums.InstanceStatus 与 svc-hall ApplyFlowStatusEnum;
|
||||||
|
* 未提交 0 为 Notice 特有——apply 尚未成功的暂态)。仅在绑定了审核流程(flow_guid 非空)时有意义。
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FlowAuditStatusEnum {
|
||||||
|
|
||||||
|
PENDING_SUBMIT(0, "待提交"),
|
||||||
|
APPROVING(10, "审批中"),
|
||||||
|
APPROVED(20, "已通过"),
|
||||||
|
REJECTED(30, "已驳回"),
|
||||||
|
WITHDRAWN(40, "已撤回");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
FlowAuditStatusEnum(int code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlowAuditStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FlowAuditStatusEnum e : values()) {
|
||||||
|
if (e.code == code) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String descOf(Integer code) {
|
||||||
|
FlowAuditStatusEnum e = of(code);
|
||||||
|
return e == null ? null : e.getDesc();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.yunhai.notice.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知状态。status=1 已发布仍是用户端唯一可见态;
|
||||||
|
* 先审后发引入 status=0 未发布(审核载体:绑流程通知提交后置 0,回调 completed 才置 1,
|
||||||
|
* 审核进度独立成列 audit_status)。未绑流程的通知仍无草稿态(直发即 1),
|
||||||
|
* 定时发送通过 timingSendTime + 查询侧「已到点」过滤控制可见性。
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum NoticeStatusEnum {
|
||||||
|
|
||||||
|
UNPUBLISHED(0, "未发布"),
|
||||||
|
PUBLISHED(1, "已发布");
|
||||||
|
|
||||||
|
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.notice.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,59 @@
|
|||||||
|
package com.yunhai.notice.job;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.notice.service.sms.SmsSendService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信到点补发调度(§3.2 / BP-R5-2):扫"已发布 + 要发短信 + 从未发过 + 已到定时点"的行补发。
|
||||||
|
* 覆盖场景:定时发送的通知在定时点之前审批通过——发布链路按设计跳过短信(可见性即时生效),
|
||||||
|
* 原实现无任何补发导致短信永久丢失。幂等由 SmsSendService 的 sms_sent_at 原子认领保证
|
||||||
|
* (并发/重复调度不双发);单条失败不阻断批次(归还认领,下轮重试)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SmsCatchupJob {
|
||||||
|
|
||||||
|
/** 单批上限(防御性:避免超大存量发布时一次性拉满) */
|
||||||
|
private static final int BATCH = 200;
|
||||||
|
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
private final SmsSendService smsSendService;
|
||||||
|
|
||||||
|
@Value("${notice.sms.catchup-enabled:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
/** fixedDelay=5min(上一轮跑完起算,单实例内天然不重叠;跨实例由原子认领闸) */
|
||||||
|
@Scheduled(fixedDelayString = "${notice.sms.catchup-fixed-delay-ms:300000}", initialDelay = 30_000)
|
||||||
|
public void catchupDueSms() {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Notice> due = noticeRepository.findSmsDue(LocalDateTime.now(), PageRequest.of(0, BATCH));
|
||||||
|
if (due.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info("[notice-sms] 到点补发扫描:{} 条待发", due.size());
|
||||||
|
int sent = 0;
|
||||||
|
for (Notice notice : due) {
|
||||||
|
try {
|
||||||
|
if (smsSendService.sendOnce(notice)) {
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[notice-sms] 补发单条失败(下轮重试)notice={}:{}", notice.getId(), e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[notice-sms] 到点补发完成:本轮发送 {} 条", sent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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.notice.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级管理接口-教师(userclass {@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,80 @@
|
|||||||
|
package com.yunhai.notice.model.flow;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowSummary;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowApplyBody;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowApplyResult;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowInstance;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowCallbackRegBody;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowCallbackRegResult;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowDiagramUrl;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowProgress;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程中心 /oapi/flow/** 契约模型。<br>
|
||||||
|
* 2026-09-16 起全部改为 {@code extends} SDK 门面 DTO({@code yh-openapi-client FlowDto},
|
||||||
|
* 与 flow-center OapiModels 逐字对齐,契约锚点:《流程中心设计概览方案.md》§8 / M1 开工说明 §5)
|
||||||
|
* ——字段单一事实源收敛到 SDK,本类仅保留 Notice 侧类型名(消费方签名零改动),
|
||||||
|
* 并可直接作为 {@code YschFlowOpenApi} 泛型出入参传递。
|
||||||
|
*/
|
||||||
|
public final class FlowModels {
|
||||||
|
|
||||||
|
private FlowModels() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程摘要(GET /oapi/flow/list 元素) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class FlowSummaryVo extends FlowSummary {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代发起请求体(POST /oapi/flow/apply) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public static class ApplyBody extends FlowApplyBody {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代发起响应(biz uk 幂等:重复 apply 返回既有办件 duplicated=true;30/40 态 R4 重启推进) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class ApplyResultVo extends FlowApplyResult {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按业务键查办件响应(GET /oapi/flow/instance;不存在为 null) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class InstanceVo extends FlowInstance {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回调注册请求体(POST /oapi/flow/callback/reg) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public static class CallbackRegBody extends FlowCallbackRegBody {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回调注册响应(secret 注册时生成下发;重复注册会轮换 secret,绝不重复调用) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class CallbackRegResultVo extends FlowCallbackRegResult {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 办件进度(GET /oapi/flow/progress;steps[0] 固定"发起申请",2026-09-17 接入) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class ProgressVo extends FlowProgress {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 流程示意图换图地址(GET /oapi/flow/diagram/url;imgUrl 为 flow-center 免签相对路径) */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class DiagramUrlVo extends FlowDiagramUrl {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.notice.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,91 @@
|
|||||||
|
package com.yunhai.notice.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;
|
||||||
|
|
||||||
|
// ===== 先审后发(编辑页驳回/撤回态展示) =====
|
||||||
|
|
||||||
|
/** 绑定的审核流程 GUID(null=直发未接流程) */
|
||||||
|
private String flowGuid;
|
||||||
|
|
||||||
|
/** 审核状态:0 待提交 / 10 审批中 / 20 通过 / 30 驳回 / 40 已撤回(null=未绑) */
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
/** 审核状态文案 */
|
||||||
|
private String auditStatusDesc;
|
||||||
|
|
||||||
|
/** 流程中心办件 id(撤回/催办直调 flow-center 用) */
|
||||||
|
private Long flowInstanceId;
|
||||||
|
|
||||||
|
/** 终态明细(result/opinion/rejectNode;null=无) */
|
||||||
|
private Object flowResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件项(旧 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.notice.model.notice;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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,55 @@
|
|||||||
|
package com.yunhai.notice.model.notice;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核状态过滤(管理端列表「状态」筛选,对应列 audit_status):
|
||||||
|
* 0 待提交 / 10 审批中 / 20 已通过 / 30 已驳回 / 40 已撤回;
|
||||||
|
* <b>-1 = 无需审核</b>(audit_status 为 NULL,未走审核轨道的直发通知);null 不过滤。
|
||||||
|
*/
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
/** 类型过滤: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,56 @@
|
|||||||
|
package com.yunhai.notice.model.notice;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import com.yunhai.notice.common.validation.ValidationGroups;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
|
||||||
|
// 审核流程不再由请求传入:绑定在「应用设置 > 流程绑定」(notice_flow_binding,按学校一条),
|
||||||
|
// 由 NoticeService 按「已绑定 + 通知对象=全部用户」判定,避免前端/请求绕过审核规则。
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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,82 @@
|
|||||||
|
package com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
|
||||||
|
private String UpFile;
|
||||||
|
|
||||||
|
// ===== 先审后发(流程中心托管) =====
|
||||||
|
|
||||||
|
/** 绑定的审核流程 GUID(null=直发,未接流程中心) */
|
||||||
|
private String flowGuid;
|
||||||
|
|
||||||
|
/** 审核状态:0 待提交 / 10 审批中 / 20 通过 / 30 驳回 / 40 已撤回(null=未绑流程) */
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
/** 审核状态文案(待提交/审批中/已通过/已驳回/已撤回;null=未绑流程) */
|
||||||
|
private String auditStatusDesc;
|
||||||
|
|
||||||
|
/** 流程中心办件 id(提交成功后非空;详情页撤回/催办直调 flow-center 用) */
|
||||||
|
private Long flowInstanceId;
|
||||||
|
|
||||||
|
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,30 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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,19 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.NoticeCc;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程抄送留痕仓储(「抄送给我的」主路径)。
|
||||||
|
*/
|
||||||
|
public interface NoticeCcRepository extends JpaRepository<NoticeCc, Integer> {
|
||||||
|
|
||||||
|
/** 按学校 + 被抄送人分页(sent_time 倒序,倒序索引 idx_notice_cc_user 覆盖) */
|
||||||
|
Page<NoticeCc> findBySchIdAndCcUserIdOrderBySentTimeDescIdDesc(Integer schId, Integer ccUserId,
|
||||||
|
Pageable pageable);
|
||||||
|
|
||||||
|
/** 未发布通知可见守卫第 4 条:当前用户是否被抄送该通知(NoticeFlowGuard) */
|
||||||
|
boolean existsByNoticeIdAndCcUserId(Integer noticeId, Integer ccUserId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.NoticeFlowBinding;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface NoticeFlowBindingRepository extends JpaRepository<NoticeFlowBinding, Integer> {
|
||||||
|
|
||||||
|
Optional<NoticeFlowBinding> findBySchId(Integer schId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.NoticeFlowEvent;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface NoticeFlowEventRepository extends JpaRepository<NoticeFlowEvent, Integer> {
|
||||||
|
|
||||||
|
boolean existsByEventId(String eventId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.NoticeFlowReg;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface NoticeFlowRegRepository extends JpaRepository<NoticeFlowReg, Integer> {
|
||||||
|
|
||||||
|
Optional<NoticeFlowReg> findBySchIdAndAppKeyAndFlowGuid(Integer schId, String appKey, String flowGuid);
|
||||||
|
|
||||||
|
Optional<NoticeFlowReg> findBySchIdAndFlowGuid(Integer schId, String flowGuid);
|
||||||
|
|
||||||
|
List<NoticeFlowReg> findBySchIdAndStatusOrderByUpdateTimeDesc(Integer schId, Integer status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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,31 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知接收人关联表仓储。
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
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);
|
||||||
|
|
||||||
|
/** 回调事件处理专用:行级悲观锁读(并发推送串行化,防整实体保存互相覆盖丢更新) */
|
||||||
|
@org.springframework.data.jpa.repository.Lock(javax.persistence.LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
@org.springframework.data.jpa.repository.Query("select n from Notice n where n.id = :id")
|
||||||
|
Optional<Notice> findByIdForUpdate(@org.springframework.data.repository.query.Param("id") Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交审核后的原子回填(窄列更新,防旧快照整实体覆盖并发回调的推进):
|
||||||
|
* 置 flow_instance_id + audit=10;若回调已并发先行落定 audit=20(审核通过)则保持 20 不回退。
|
||||||
|
*/
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"update Notice n set n.flowInstanceId = :instanceId, "
|
||||||
|
+ "n.auditStatus = (case when n.auditStatus = 20 then n.auditStatus else 10 end) "
|
||||||
|
+ "where n.id = :id")
|
||||||
|
int markApproving(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||||
|
@org.springframework.data.repository.query.Param("instanceId") Long instanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调推进在途态的窄列更新(2026-10-17 ysch-flow Adapter.persist 落点):只写 audit_status +
|
||||||
|
* last_instance_version,绝不整实体 save——同事务里 apply 返回的 markApproving 与并发推送
|
||||||
|
* (E2E 教训:整实体盲写会把已落定的 20 打回 10 并覆盖结果快照)。
|
||||||
|
*/
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"update Notice n set n.auditStatus = :status, n.lastInstanceVersion = :version where n.id = :id")
|
||||||
|
int updateAuditFromFlow(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||||
|
@org.springframework.data.repository.query.Param("status") Integer status,
|
||||||
|
@org.springframework.data.repository.query.Param("version") Integer version);
|
||||||
|
|
||||||
|
/** 回调落终态:审核码 + 版本 + 结果明细快照(flow_result_json,详情页进度卡片数据源) */
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"update Notice n set n.auditStatus = :status, n.lastInstanceVersion = :version, "
|
||||||
|
+ "n.flowResultJson = :result where n.id = :id")
|
||||||
|
int updateAuditResultFromFlow(@org.springframework.data.repository.query.Param("id") Integer id,
|
||||||
|
@org.springframework.data.repository.query.Param("status") Integer status,
|
||||||
|
@org.springframework.data.repository.query.Param("version") Integer version,
|
||||||
|
@org.springframework.data.repository.query.Param("result") String result);
|
||||||
|
|
||||||
|
/** 审核通过=正式发布(status 0→1,用户端自此可见);窄列更新,与审核码同事务但分行 */
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query("update Notice n set n.status = 1 where n.id = :id")
|
||||||
|
int markPublished(@org.springframework.data.repository.query.Param("id") Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信发送原子认领(§3.2 防重复核心):置 sms_sent_at,仅当当前为 NULL(未发)时生效。
|
||||||
|
* 返回 0=已被发送过(并发/重复调度双发的闸门)。发送失败由调用方 clearSmsSent 归还。
|
||||||
|
*/
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"update Notice n set n.smsSentAt = :sentAt where n.id = :id and n.smsSentAt is null")
|
||||||
|
int markSmsSent(@Param("id") Integer id, @Param("sentAt") LocalDateTime sentAt);
|
||||||
|
|
||||||
|
/** 发送失败归还认领(下轮补发调度可重试) */
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.data.jpa.repository.Modifying
|
||||||
|
@org.springframework.data.jpa.repository.Query(
|
||||||
|
"update Notice n set n.smsSentAt = null where n.id = :id and n.smsSentAt is not null")
|
||||||
|
int clearSmsSent(@Param("id") Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 到点待补发短信清单(§3.2 SmsCatchupJob 扫描):已发布 + 要发短信 + 从未发过 + 已到定时点
|
||||||
|
* (无定时点即发;覆盖"审批通过早于定时点被跳过"的行——BP-R5-2 短信永久丢失缺口)。
|
||||||
|
*/
|
||||||
|
@Query("select n from Notice n where n.status = 1 and n.smsFlag = 1 and n.smsSentAt is null"
|
||||||
|
+ " and (n.timingSendTime is null or n.timingSendTime <= :now) order by n.id asc")
|
||||||
|
List<Notice> findSmsDue(@Param("now") LocalDateTime now,
|
||||||
|
org.springframework.data.domain.Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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.notice.repository;
|
||||||
|
|
||||||
|
import com.yunhai.notice.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,20 @@
|
|||||||
|
package com.yunhai.notice.service;
|
||||||
|
|
||||||
|
import com.yunhai.notice.model.SchTeaList;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级管理(userclass)开放接口客户端。
|
||||||
|
* 2026-09-16 起委托 SDK 类型化门面 {@code YschClassOpenApi}(服务目录 userclass),当前仅提供教师查询。
|
||||||
|
*/
|
||||||
|
public interface IClassService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定学校全部教师(含所属组 GroupIds)。
|
||||||
|
* 对应 userclass 开放接口:GET {@code {userclass}/oapi/DbClass/GetSchteaList?Group={schId}}
|
||||||
|
*
|
||||||
|
* @param schId 学校ID(即用户当前身份的 schId)
|
||||||
|
*/
|
||||||
|
List<SchTeaList> getSchTeaList(int schId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.yunhai.notice.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.notice.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.notice.service;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.UpFile;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.notice.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,142 @@
|
|||||||
|
package com.yunhai.notice.service;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.model.BasePageResult;
|
||||||
|
import com.yunhai.notice.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.timingSendTime 不为空且晚于当前时间,则作为定时发送,
|
||||||
|
* 未到点前用户端收件箱不可见(到点后由查询过滤自然可见)。
|
||||||
|
*
|
||||||
|
* @return 新建通知 ID(用于后续回填附件 TopicID 等)
|
||||||
|
*/
|
||||||
|
Integer publish(NoticeRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑通知:按 id 更新标题/内容/类型/部门/置顶,发布人与状态保持不变。
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户端列表:仅展示已发布且已到点的通知,并标记当前用户是否已读,附未读总数。
|
||||||
|
* 返回 NoticePageResult(扩展 unreadCount)。
|
||||||
|
*/
|
||||||
|
NoticePageResult<NoticeVo> userList(NoticeQuery query, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端列表:按条件查询全部通知(管理员查看全部)。
|
||||||
|
*/
|
||||||
|
BasePageResult<NoticeVo> adminList(NoticeQuery query, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。
|
||||||
|
*/
|
||||||
|
NoticeVo detail(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑回显详情(不标记已读)。
|
||||||
|
*/
|
||||||
|
NoticeVo getDetail(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记已读:为当前用户写入(幂等)该通知的阅读记录。
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阅读情况分组(已读/未读)。
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getReadDetailGrouped(Integer serid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 催收未读:枚举未读接收人,后续接入消息推送。
|
||||||
|
*/
|
||||||
|
void expedite(Integer serid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。
|
||||||
|
*/
|
||||||
|
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.notice.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.notice.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.notice.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>userclass(班级管理)获取该校全部教师(/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>从基础平台拉取当前用户模块树(含学校上下文);<b>层级由用户中心 {@code BoaModule.children} 下发</b>,
|
||||||
|
* 本侧仅映射为前端 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,249 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
import com.yunhai.platform.appauth.AppSignInterceptor;
|
||||||
|
import com.yunhai.platform.appauth.LocalAppIdentityProvider;
|
||||||
|
import com.yunhai.platform.openapi.core.TargetSpec;
|
||||||
|
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlowClient 默认实现:主路径走 SDK 类型化门面 {@link YschFlowOpenApi}(服务目录 key=
|
||||||
|
*{@code flow-center}:yh.call.targets > yh.open.services > 枚举 base-host 兜底,
|
||||||
|
* 《子应用配置优化方案》§4.2 零专属地址配置);目录未显式配置时回落本应用 legacy 键
|
||||||
|
* {@code notice.flow.base-url}(ad-hoc TargetSpec,迁移期兼容)。签名/信封解析/错误映射
|
||||||
|
* 全部由 SDK 统一承载(401/403→DException、code!=0→DException、传输错→OpenApiException)。
|
||||||
|
* {@code notice.flow.enabled=false} 时全部方法抛业务异常(文案含"流程中心未接入")供前端降级。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FlowCenterClient implements FlowClient {
|
||||||
|
|
||||||
|
/** flow-center 业务类型常量(契约冻结:bizType="notice"、bizId=notice.id) */
|
||||||
|
public static final String BIZ_TYPE = "notice";
|
||||||
|
|
||||||
|
private static final String NOT_CONNECTED = "流程中心未接入,该功能不可用";
|
||||||
|
|
||||||
|
private final NoticeFlowProperties properties;
|
||||||
|
private final YschOpenCall openCall;
|
||||||
|
|
||||||
|
private final boolean directoryResolved;
|
||||||
|
|
||||||
|
public FlowCenterClient(NoticeFlowProperties properties,
|
||||||
|
ObjectProvider<YschOpenCall> openCallProvider,
|
||||||
|
ObjectProvider<LocalAppIdentityProvider> identityProvider,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.properties = properties;
|
||||||
|
YschOpenCall call = openCallProvider.getIfAvailable();
|
||||||
|
if (call == null) {
|
||||||
|
// 兜底自建(正常路径由 yh-openapi-starter 装配 YschOpenCall bean);identity 缺失时
|
||||||
|
// AppSignInterceptor 自动跳过签名(请求裸发,enforce 端会 401——配置问题尽早暴露)
|
||||||
|
call = new YschOpenCall(new com.yunhai.platform.openapi.core.YschCallProperties(), objectMapper,
|
||||||
|
(host, basePath) -> java.util.Collections.singletonList(
|
||||||
|
(org.springframework.http.client.ClientHttpRequestInterceptor)
|
||||||
|
new AppSignInterceptor(identityProvider.getIfAvailable(), host, basePath)));
|
||||||
|
}
|
||||||
|
this.openCall = call;
|
||||||
|
// 服务目录显式配置(targets/services)则走门面命名目标;否则回落 legacy base-url ad-hoc
|
||||||
|
this.directoryResolved = call.hasTarget(com.yunhai.platform.openapi.core.PlatformService.FLOW_CENTER.key());
|
||||||
|
if (directoryResolved) {
|
||||||
|
com.yunhai.platform.openapi.flow.YschFlowOpenApi.setCall(call);
|
||||||
|
}
|
||||||
|
String legacy = properties.getBaseUrl() == null ? "" : properties.getBaseUrl().trim();
|
||||||
|
if (properties.isEnabled() && !directoryResolved && legacy.isEmpty()) {
|
||||||
|
// 目录未配且 legacy 键也空:构造期 fail-fast(本键已无默认值),
|
||||||
|
// 避免静默打 localhost 把"地址没配"伪装成"流程中心不可达"。
|
||||||
|
// enabled=false(显式未接入)时不校验——地址本就不需要。
|
||||||
|
throw new IllegalStateException("流程中心调用地址未配置:请配置服务目录 yh.open.services."
|
||||||
|
+ com.yunhai.platform.openapi.core.PlatformService.FLOW_CENTER.key()
|
||||||
|
+ "(推荐,容器直连完整 URL)或回落键 notice.flow.base-url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FlowModels.FlowSummaryVo> listFlows(Integer schId) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.listFlows(
|
||||||
|
schId, FlowModels.FlowSummaryVo.class);
|
||||||
|
}
|
||||||
|
return openCall.getList(target(), "/oapi/flow/list", params("schId", schId),
|
||||||
|
FlowModels.FlowSummaryVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FlowModels.ApplyResultVo apply(Integer schId, FlowModels.ApplyBody body) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.apply(
|
||||||
|
schId, body, FlowModels.ApplyResultVo.class);
|
||||||
|
}
|
||||||
|
return openCall.post(target(), "/oapi/flow/apply", params("schId", schId), body,
|
||||||
|
FlowModels.ApplyResultVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FlowModels.InstanceVo getInstanceByBiz(Integer schId, String bizType, String bizId) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.instance(
|
||||||
|
bizType, bizId, schId, FlowModels.InstanceVo.class);
|
||||||
|
}
|
||||||
|
Map<String, String> query = new HashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("bizType", bizType);
|
||||||
|
query.put("bizId", bizId);
|
||||||
|
return openCall.get(target(), "/oapi/flow/instance", query, FlowModels.InstanceVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FlowModels.CallbackRegResultVo registerCallback(Integer schId, FlowModels.CallbackRegBody body) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.registerCallback(
|
||||||
|
schId, body, FlowModels.CallbackRegResultVo.class);
|
||||||
|
}
|
||||||
|
return openCall.post(target(), "/oapi/flow/callback/reg", params("schId", schId), body,
|
||||||
|
FlowModels.CallbackRegResultVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void withdraw(Integer schId, Long instanceId, Integer submitterId) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
com.yunhai.platform.openapi.flow.YschFlowOpenApi.withdraw(schId, instanceId, submitterId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, String> query = new HashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("submitterId", String.valueOf(submitterId));
|
||||||
|
// 无请求体:对端 POST 无 @RequestBody,query 传参即可(body 传 null 即零长度请求体)
|
||||||
|
openCall.post(target(), "/oapi/flow/instance/" + instanceId + "/withdraw", query, null, Void.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FlowModels.ProgressVo progress(Integer schId, Long instanceId) {
|
||||||
|
requireEnabled();
|
||||||
|
if (directoryResolved) {
|
||||||
|
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.progress(
|
||||||
|
instanceId, BIZ_TYPE, null, schId, FlowModels.ProgressVo.class);
|
||||||
|
}
|
||||||
|
Map<String, String> query = new HashMap<>();
|
||||||
|
query.put("instanceId", String.valueOf(instanceId));
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
return openCall.get(target(), "/oapi/flow/progress", query, FlowModels.ProgressVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 免签出图端点(flow-center OpenDiagramImageController,相对其 context-path) */
|
||||||
|
private static final String DIAGRAM_IMG_PATH = "/open/flow/diagram/img";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] diagramImage(Integer schId, Long instanceId) {
|
||||||
|
requireEnabled();
|
||||||
|
String imgUrl;
|
||||||
|
if (directoryResolved) {
|
||||||
|
FlowModels.DiagramUrlVo dto = com.yunhai.platform.openapi.flow.YschFlowOpenApi.diagramUrl(
|
||||||
|
instanceId, null, null, schId, FlowModels.DiagramUrlVo.class);
|
||||||
|
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||||
|
} else {
|
||||||
|
Map<String, String> query = new HashMap<>();
|
||||||
|
query.put("instanceId", String.valueOf(instanceId));
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
FlowModels.DiagramUrlVo dto =
|
||||||
|
openCall.get(target(), "/oapi/flow/diagram/url", query, FlowModels.DiagramUrlVo.class);
|
||||||
|
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||||
|
}
|
||||||
|
String token = tokenOf(imgUrl);
|
||||||
|
if (token == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心未返回有效的示意图地址");
|
||||||
|
}
|
||||||
|
Map<String, String> imgQuery = new HashMap<>();
|
||||||
|
imgQuery.put("token", token);
|
||||||
|
byte[] bytes = directoryResolved
|
||||||
|
? com.yunhai.platform.openapi.flow.YschFlowOpenApi.raw().download(DIAGRAM_IMG_PATH, imgQuery)
|
||||||
|
: openCall.download(target(), DIAGRAM_IMG_PATH, imgQuery);
|
||||||
|
if (bytes == null || bytes.length == 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程示意图为空(流程可能未发布节点)");
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从换图地址中取 token(形如 {@code /open/flow/diagram/img?token=xxx})。
|
||||||
|
* 包可见:契约测试冻结"token 只在后端流转、前端拿不到"的取用形态。
|
||||||
|
*/
|
||||||
|
static String tokenOf(String imgUrl) {
|
||||||
|
if (imgUrl == null || imgUrl.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int q = imgUrl.indexOf("?token=");
|
||||||
|
if (q < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String token = imgUrl.substring(q + "?token=".length());
|
||||||
|
int amp = token.indexOf('&');
|
||||||
|
if (amp >= 0) {
|
||||||
|
token = token.substring(0, amp);
|
||||||
|
}
|
||||||
|
return token.isEmpty() ? null : token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] diagramImageByFlowGuid(Integer schId, String flowGuid) {
|
||||||
|
requireEnabled();
|
||||||
|
String imgUrl;
|
||||||
|
if (directoryResolved) {
|
||||||
|
FlowModels.DiagramUrlVo dto = com.yunhai.platform.openapi.flow.YschFlowOpenApi.diagramUrl(
|
||||||
|
null, flowGuid, null, schId, FlowModels.DiagramUrlVo.class);
|
||||||
|
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||||
|
} else {
|
||||||
|
Map<String, String> query = new HashMap<>();
|
||||||
|
query.put("flowGuid", flowGuid);
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
FlowModels.DiagramUrlVo dto =
|
||||||
|
openCall.get(target(), "/oapi/flow/diagram/url", query, FlowModels.DiagramUrlVo.class);
|
||||||
|
imgUrl = dto == null ? null : dto.getImgUrl();
|
||||||
|
}
|
||||||
|
String token = tokenOf(imgUrl);
|
||||||
|
if (token == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心未返回有效的示意图地址");
|
||||||
|
}
|
||||||
|
Map<String, String> imgQuery = new HashMap<>();
|
||||||
|
imgQuery.put("token", token);
|
||||||
|
byte[] bytes = directoryResolved
|
||||||
|
? com.yunhai.platform.openapi.flow.YschFlowOpenApi.raw().download(DIAGRAM_IMG_PATH, imgQuery)
|
||||||
|
: openCall.download(target(), DIAGRAM_IMG_PATH, imgQuery);
|
||||||
|
if (bytes == null || bytes.length == 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程示意图为空(流程可能未发布节点)");
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetSpec target() {
|
||||||
|
// base-path 空:本地/容器裸地址直连(两级模型 §2.2 对齐不变式的现行约定)
|
||||||
|
return TargetSpec.of(properties.getBaseUrl(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireEnabled() {
|
||||||
|
if (!properties.isEnabled()) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, NOT_CONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> params(String k, Object v) {
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
if (k != null && v != null) {
|
||||||
|
m.put(k, String.valueOf(v));
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程中心出站接缝(Notice 首接方案 §5.1,方法面与 svc-hall FlowCenterClient 一致)。
|
||||||
|
* 接口保留三重意义:防依赖(业务层不感知 YschOpenCall)、可测试(fake 注入)、可降级(enabled 开关)。
|
||||||
|
*/
|
||||||
|
public interface FlowClient {
|
||||||
|
|
||||||
|
/** 已发布流程列表(编辑页"审核流程"选择器数据源) */
|
||||||
|
List<FlowModels.FlowSummaryVo> listFlows(Integer schId);
|
||||||
|
|
||||||
|
/** 代发起(biz uk 幂等:重复 apply 同办件;30/40 态 R4 重启推进) */
|
||||||
|
FlowModels.ApplyResultVo apply(Integer schId, FlowModels.ApplyBody body);
|
||||||
|
|
||||||
|
/** 按业务键查办件(进度卡片/放行判定/对账兜底;不存在返回 null) */
|
||||||
|
FlowModels.InstanceVo getInstanceByBiz(Integer schId, String bizType, String bizId);
|
||||||
|
|
||||||
|
/** 回调注册(下发 secret;重复注册会轮换 secret——仅在 reg 缺失时调用) */
|
||||||
|
FlowModels.CallbackRegResultVo registerCallback(Integer schId, FlowModels.CallbackRegBody body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回(托管件,2026-09-17 接入):对端按 submitterId 与办件提交人逐字复核,
|
||||||
|
* 应用签名只证明"哪个应用在调"不证明"谁在撤"——防业务应用越权代撤他人办件。
|
||||||
|
*/
|
||||||
|
void withdraw(Integer schId, Long instanceId, Integer submitterId);
|
||||||
|
|
||||||
|
/** 办件进度(结构化 steps,instanceId 定位;oapi 只校验 schId,调用方先做可见性守卫) */
|
||||||
|
FlowModels.ProgressVo progress(Integer schId, Long instanceId);
|
||||||
|
|
||||||
|
/** 流程示意图字节(token 换图两步都在本方法内,浏览器只拿同源 PNG,token 不落前端) */
|
||||||
|
byte[] diagramImage(Integer schId, Long instanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程定义示意图字节(def 口径,绑定页/发通知页 Drawer):按 flowGuid 取最高已发布版出图。
|
||||||
|
* 与办件图同一免签 token 通道;本校限定由 flow-center 按 schId+flowGuid 收敛(绑定已解除/
|
||||||
|
* 无已发布版 → 业务异常文案,不留 500)。
|
||||||
|
*/
|
||||||
|
byte[] diagramImageByFlowGuid(Integer schId, String flowGuid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.yunhai.platform.openapi.core.PlatformService;
|
||||||
|
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内审批出站客户端(第三步 T-C4):待办/办件查询 + 通过/驳回,经网关打 flow-center
|
||||||
|
* {@code /oapi/flow/tasks|task/**}(2026-09-23 已上线)。
|
||||||
|
* <p>
|
||||||
|
* <b>刻意用 {@link YschOpenCall} 泛化调用而非 YschFlowOpenApi 新门面方法</b>——四方法
|
||||||
|
* (todoTasks/instances/approveTask/rejectTask)在 SDK main 已编码但**未发版**,notice 运行时
|
||||||
|
* 依赖的是 nexus 已发布快照;泛化调用与 FlowCenterClient legacy 分支同一条
|
||||||
|
* resolveTarget 解析链(targets > services > 枚举 base-host 兜底=网关),零发版依赖。
|
||||||
|
* SDK 发版后可平替为门面方法(行为等价,测试锁 URL 不变)。
|
||||||
|
* <p>
|
||||||
|
* 身份口径:operatorId/operatorName 由调用方(controller)从 CAS 登录态取,本类不猜身份;
|
||||||
|
* 对端按任务办理人逐字复核(非办理人→业务码拒绝),notice 不重复校验。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class NoticeApprovalClient {
|
||||||
|
|
||||||
|
private static final String TARGET = PlatformService.FLOW_CENTER.key();
|
||||||
|
private static final String NOT_CONNECTED = "流程中心未接入,审批功能不可用";
|
||||||
|
|
||||||
|
private final NoticeFlowProperties properties;
|
||||||
|
private final YschOpenCall openCall;
|
||||||
|
|
||||||
|
public NoticeApprovalClient(NoticeFlowProperties properties,
|
||||||
|
ObjectProvider<YschOpenCall> openCallProvider) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.openCall = openCallProvider.getIfAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的待办(bizType 固定 notice):Map 形态即 flow-center BasePageResult JSON(total/content/…) */
|
||||||
|
public Map<String, Object> todoTasks(Integer schId, Integer assigneeId, Integer page, Integer size) {
|
||||||
|
requireEnabled();
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("assigneeId", String.valueOf(assigneeId));
|
||||||
|
query.put("bizType", FlowCenterClient.BIZ_TYPE);
|
||||||
|
if (page != null) {
|
||||||
|
query.put("page", String.valueOf(page));
|
||||||
|
}
|
||||||
|
if (size != null) {
|
||||||
|
query.put("size", String.valueOf(size));
|
||||||
|
}
|
||||||
|
return openCall.get(TARGET, "/oapi/flow/tasks/todo", query, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 办件分页(bizType 固定 notice;status 可空) */
|
||||||
|
public Map<String, Object> instances(Integer schId, Integer status, Integer page, Integer size) {
|
||||||
|
requireEnabled();
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("bizType", FlowCenterClient.BIZ_TYPE);
|
||||||
|
if (status != null) {
|
||||||
|
query.put("status", String.valueOf(status));
|
||||||
|
}
|
||||||
|
if (page != null) {
|
||||||
|
query.put("page", String.valueOf(page));
|
||||||
|
}
|
||||||
|
if (size != null) {
|
||||||
|
query.put("size", String.valueOf(size));
|
||||||
|
}
|
||||||
|
return openCall.get(TARGET, "/oapi/flow/instances", query, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前人在本通知上的待办(v2.1 业务详情卡片数据源,{@code /oapi/flow/tasks/by-biz})。
|
||||||
|
* <p>返回 Map 即对端 {@code ByBizTasksVo} JSON:{@code tasks[]} 0/1/N 三种计数都走 HTTP 200,
|
||||||
|
* <b>调用方不得据 tasks 反推"这个通知上有没有别人的待办"</b>——办件不存在、错学校、错 bizType、
|
||||||
|
* 非办理人四种情形出参完全同形(都是空 tasks),这是对端的防探测设计,本端不要试图区分。
|
||||||
|
* <p>{@code currentNodes} 是节点 <b>id</b> 逗号串(办件原文口径),人读名称取 {@code tasks[].nodeName}。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<String, Object> byBiz(Integer schId, Integer operatorId, Integer noticeId) {
|
||||||
|
requireEnabled();
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("bizType", FlowCenterClient.BIZ_TYPE);
|
||||||
|
query.put("bizId", String.valueOf(noticeId));
|
||||||
|
query.put("operatorId", String.valueOf(operatorId));
|
||||||
|
return openCall.get(TARGET, "/oapi/flow/tasks/by-biz", query, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整办理任务页地址({@code /oapi/flow/embed/url?mode=task}):返回 CAS 会话页
|
||||||
|
* {@code {front}/#/task/{taskId}},<b>无 token</b>(写面,办理权由该页加载时的用户面接口按
|
||||||
|
* assigneeId 复核)。仍走 {@link YschOpenCall} 泛化调用——nexus 已发布快照的
|
||||||
|
* {@code YschFlowOpenApi} 只有 embedUrl/todoTasks/instances,没有 taskPageUrl 门面方法,
|
||||||
|
* 而本批纪律是不动 BasePlatform。SDK 补齐后可平替(URL 不变,测试锁死)。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<String, Object> taskPageUrl(Integer schId, Long taskId) {
|
||||||
|
requireEnabled();
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("mode", "task");
|
||||||
|
query.put("taskId", String.valueOf(taskId));
|
||||||
|
return openCall.get(TARGET, "/oapi/flow/embed/url", query, Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同意(opinion 可空;多出口 edgeName 透传) */
|
||||||
|
public void approve(Long taskId, Integer schId, Integer operatorId, String operatorName,
|
||||||
|
String opinion, String edgeName) {
|
||||||
|
requireEnabled();
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
if (opinion != null && !opinion.trim().isEmpty()) {
|
||||||
|
body.put("opinion", opinion.trim());
|
||||||
|
}
|
||||||
|
if (edgeName != null && !edgeName.trim().isEmpty()) {
|
||||||
|
body.put("edgeName", edgeName.trim());
|
||||||
|
}
|
||||||
|
openCall.post(TARGET, "/oapi/flow/task/" + taskId + "/approve",
|
||||||
|
operatorQuery(schId, operatorId, operatorName), body, Void.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝(opinion 必填由对端强制;此处前置校验给前端更快的反馈) */
|
||||||
|
public void reject(Long taskId, Integer schId, Integer operatorId, String operatorName, String opinion) {
|
||||||
|
requireEnabled();
|
||||||
|
if (opinion == null || opinion.trim().isEmpty()) {
|
||||||
|
throw new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "驳回必须填写意见");
|
||||||
|
}
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("opinion", opinion.trim());
|
||||||
|
openCall.post(TARGET, "/oapi/flow/task/" + taskId + "/reject",
|
||||||
|
operatorQuery(schId, operatorId, operatorName), body, Void.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> operatorQuery(Integer schId, Integer operatorId, String operatorName) {
|
||||||
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
|
query.put("schId", String.valueOf(schId));
|
||||||
|
query.put("operatorId", String.valueOf(operatorId));
|
||||||
|
query.put("operatorName", operatorName);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireEnabled() {
|
||||||
|
if (!properties.isEnabled() || openCall == null) {
|
||||||
|
throw new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.FORBIDDEN, NOT_CONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.entity.NoticeCc;
|
||||||
|
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
import com.yunhai.notice.repository.NoticeCcRepository;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.notice.service.sms.SmsSendService;
|
||||||
|
import com.yunhai.platform.flowcallback.FlowCallbackRegistrar;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowApplyBody;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowCallbackEvent;
|
||||||
|
import com.yunhai.platform.yschflow.FlowApplyGateway;
|
||||||
|
import com.yunhai.platform.yschflow.FlowBizAdapter;
|
||||||
|
import com.yunhai.platform.yschflow.FlowBizIds;
|
||||||
|
import com.yunhai.platform.yschflow.FlowBizProperties;
|
||||||
|
import com.yunhai.platform.yschflow.FlowStateUpdate;
|
||||||
|
import com.yunhai.platform.yschflow.FlowSubmitTemplate;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知侧语义接入声明(2026-10-17 ysch-flow T3 迁移试点):把 {@code NoticeFlowCallbackHandler}
|
||||||
|
* 手写版的审核状态机推进改成一次声明,纪律(乱序/终态不回退/脏消息/抄送)由 starter 内置。
|
||||||
|
*
|
||||||
|
* <p><b>语义逐条对应(迁移即等价,不改行为)</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>行锁串行化:loader 用 {@code findByIdForUpdate}(并发推送排队,防丢更新);</li>
|
||||||
|
* <li>未绑流程/单据不存在:loader 返回 null → starter 按确定性脏消息吞掉(回 200 不重推);</li>
|
||||||
|
* <li>单据级乱序二道门:{@code recordInstanceVersion=last_instance_version};</li>
|
||||||
|
* <li>终态不回退:{@code status=audit_status},starter 判定后终态行的非终态/重复/冲突事件一律忽略;</li>
|
||||||
|
* <li>窄列原子更新:persist 走 {@code updateAuditFromFlow/updateAuditResultFromFlow},
|
||||||
|
* 绝不整实体 save(E2E 教训:apply 返回的 markApproving 与回调并发,盲写会把已落定的 20 打回 10);</li>
|
||||||
|
* <li>审核通过=正式发布 + 短信后置:onPassed 钩子(status 非 1 才置 1 并补发短信,
|
||||||
|
* 未到定时点不发——由 SmsCatchupJob 到点补发,BP-R5-2 口径);</li>
|
||||||
|
* <li>抄送留痕:notice 有自家「抄送我的」列表,故显式 {@code onCc} 镜像到 notice_cc
|
||||||
|
* (F7 默认不镜像,本应用属"确需自家展示才开 mirror"的那一类)。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>刻意未接的一项</b>:{@code recordInstanceId}/{@code recordBizId} 不接线——notice 的
|
||||||
|
* bizId 恒为 {@code notice.id}(无 attempt 段),迁移前也不做实例归属核对;接上会让"apply 已成功、
|
||||||
|
* markApproving 未提交"窗口内的首轮 submitted 事件被丢弃。按《任务书》§0.5 保守方向(少改、
|
||||||
|
* 语义等价)保留原判定面,attempt 归属留给 notice 引入重提换 attempt 的那一版再开。</p>
|
||||||
|
*
|
||||||
|
* <p>回退绳:删掉 yaml 的 {@code yh.flow-biz} 块并给 {@code NoticeFlowCallbackHandler} 加回
|
||||||
|
* {@code @Service},即回到 2026-09-20 的手写 Handler 形态(两者互斥:上下文里已存在手写
|
||||||
|
* FlowCallbackHandler 时 starter 的桥不装配)。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class NoticeFlowBizAdapter {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FlowBizAdapter<Notice> noticeFlowBiz(NoticeRepository noticeRepository,
|
||||||
|
NoticeCcRepository noticeCcRepository,
|
||||||
|
SmsSendService smsSendService,
|
||||||
|
FlowBizProperties properties) {
|
||||||
|
return FlowBizAdapter.<Notice>builder()
|
||||||
|
.bizType(FlowCenterClient.BIZ_TYPE)
|
||||||
|
// 状态码以 yaml yh.flow-biz.states 为单一真源(避免两处码表)
|
||||||
|
.states(properties.resolveStates())
|
||||||
|
.loader(bizId -> loadForUpdate(noticeRepository, bizId))
|
||||||
|
.status(Notice::getAuditStatus)
|
||||||
|
.recordInstanceVersion(Notice::getLastInstanceVersion)
|
||||||
|
.persist((notice, update) -> persistAudit(noticeRepository, notice, update))
|
||||||
|
.onPassed((notice, event) -> publishAndSms(noticeRepository, smsSendService, notice))
|
||||||
|
.onCc((notice, event) -> recordCc(noticeCcRepository, notice, event))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起编排模板:出站仍走本应用既有 {@link FlowClient}(保留 enabled=false 降级与
|
||||||
|
* legacy {@code notice.flow.base-url} 回落两条现状语义),AFTER_COMMIT/独立线程/串行/
|
||||||
|
* duplicated 按成功等编排纪律交给 starter。
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public FlowSubmitTemplate flowSubmitTemplate(FlowBizProperties properties, FlowClient flowClient,
|
||||||
|
ObjectProvider<FlowCallbackRegistrar> registrarProvider) {
|
||||||
|
return new FlowSubmitTemplate(properties, gateway(flowClient), null, registrarProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出站网关:starter 组装的请求体转投本应用既有 {@link FlowClient}(保留 enabled=false 降级与
|
||||||
|
* legacy base-url 回落),公开供单测复用同一根线(测试替身与生产装配零分叉)。
|
||||||
|
*/
|
||||||
|
public static FlowApplyGateway gateway(FlowClient flowClient) {
|
||||||
|
return new FlowApplyGateway() {
|
||||||
|
@Override
|
||||||
|
public com.yunhai.platform.openapi.flow.model.FlowApplyResult apply(long schId, FlowApplyBody body) {
|
||||||
|
return flowClient.apply(Integer.valueOf((int) schId), toNoticeBody(body));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** starter 组装的请求体转成本应用契约类型(字段逐项照搬,Notice 未用发起时指派,assigns 不透传) */
|
||||||
|
private static FlowModels.ApplyBody toNoticeBody(FlowApplyBody source) {
|
||||||
|
FlowModels.ApplyBody target = new FlowModels.ApplyBody();
|
||||||
|
target.setFlowGuid(source.getFlowGuid());
|
||||||
|
target.setBizType(source.getBizType());
|
||||||
|
target.setBizId(source.getBizId());
|
||||||
|
target.setBizTitle(source.getBizTitle());
|
||||||
|
target.setBizUrl(source.getBizUrl());
|
||||||
|
target.setBizSnapshot(source.getBizSnapshot());
|
||||||
|
target.setSubmitterId(source.getSubmitterId());
|
||||||
|
target.setSubmitterName(source.getSubmitterName());
|
||||||
|
target.setFormData(source.getFormData());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 行锁读单据;bizId 非法 / 通知不存在 / 未绑流程 → null(确定性丢弃,回 200) */
|
||||||
|
private static Notice loadForUpdate(NoticeRepository repository, String bizId) {
|
||||||
|
Integer noticeId = FlowBizIds.asInteger(bizId);
|
||||||
|
if (noticeId == null) {
|
||||||
|
throw new IllegalArgumentException("bizId 非通知主键:" + bizId);
|
||||||
|
}
|
||||||
|
Notice notice = repository.findByIdForUpdate(noticeId).orElse(null);
|
||||||
|
if (notice == null || !StringUtils.hasText(notice.getFlowGuid())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void persistAudit(NoticeRepository repository, Notice notice, FlowStateUpdate update) {
|
||||||
|
Integer version = update.getInstanceVersion() == null
|
||||||
|
? notice.getLastInstanceVersion() : update.getInstanceVersion();
|
||||||
|
if (update.isTerminal()) {
|
||||||
|
repository.updateAuditResultFromFlow(notice.getId(), update.getCode(), version, update.getSnapshotJson());
|
||||||
|
} else {
|
||||||
|
repository.updateAuditFromFlow(notice.getId(), update.getCode(), version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核通过=正式发布:置 status=1(用户端自此可见)+ 短信后置(只在本次真的从未发布→已发布时补发) */
|
||||||
|
private static void publishAndSms(NoticeRepository repository, SmsSendService smsSendService, Notice notice) {
|
||||||
|
boolean justPublished = notice.getStatus() == null
|
||||||
|
|| notice.getStatus() != NoticeStatusEnum.PUBLISHED.getCode();
|
||||||
|
if (!justPublished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repository.markPublished(notice.getId());
|
||||||
|
if (notice.getSmsFlag() == null || notice.getSmsFlag() != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 与直发链路 trySendSms 的 isImmediatePublish 语义对齐:定时未到点的通知此刻不发
|
||||||
|
// (可见性已生效),到点由 SmsCatchupJob 补发(§3.2 修 BP-R5-2 短信永久丢失缺口)
|
||||||
|
if (notice.getTimingSendTime() != null && notice.getTimingSendTime().isAfter(LocalDateTime.now())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
smsSendService.sendOnce(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抄送留痕(逐人一行,不动审核状态机):data 缺 ccUserId 属上游脏数据,记日志跳过(回 200),
|
||||||
|
* 落库异常照旧外抛回 5xx 重试。重放由 starter 的 eventId 幂等拦截。
|
||||||
|
*/
|
||||||
|
private static void recordCc(NoticeCcRepository ccRepository, Notice notice, FlowCallbackEvent event) {
|
||||||
|
Map<String, Object> data = event.getData();
|
||||||
|
Object ccUserId = data == null ? null : data.get("ccUserId");
|
||||||
|
if (ccUserId == null || String.valueOf(ccUserId).isEmpty()) {
|
||||||
|
log.warn("[notice-flow] cc.sent 缺 ccUserId(丢弃):notice={} eventId={}",
|
||||||
|
notice.getId(), event.getEventId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NoticeCc cc = new NoticeCc();
|
||||||
|
cc.setSchId(notice.getSchId());
|
||||||
|
cc.setNoticeId(notice.getId());
|
||||||
|
cc.setFlowInstanceId(event.getInstanceId());
|
||||||
|
cc.setNodeId(data.get("nodeId") == null ? null : String.valueOf(data.get("nodeId")));
|
||||||
|
cc.setNodeName(data.get("nodeName") == null ? null : String.valueOf(data.get("nodeName")));
|
||||||
|
cc.setCcUserId(Integer.valueOf(String.valueOf(ccUserId)));
|
||||||
|
cc.setCcUserName(data.get("ccUserName") == null ? null : String.valueOf(data.get("ccUserName")));
|
||||||
|
cc.setSentTime(LocalDateTime.now());
|
||||||
|
ccRepository.save(cc);
|
||||||
|
log.info("[notice-flow] 抄送留痕:notice={} instance={} ccUser={} node={}",
|
||||||
|
notice.getId(), event.getInstanceId(), cc.getCcUserId(), cc.getNodeName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.enums.FlowAuditStatusEnum;
|
||||||
|
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.platform.flowcallback.FlowCallbackHandler;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowCallbackEvent;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程中心回调业务映射(2026-09-20 迁 yh-flow-callback-spring-boot-starter):
|
||||||
|
* 验签(平台 X-App-*,/oapi/* 强制)、ping、必填校验、bizType 过滤、eventId 幂等、
|
||||||
|
* instanceVersion 乱序门由 starter 接收管线承担;本类只做通知侧状态机推进——
|
||||||
|
* <br>事件映射:submitted/task.created/task.handled→10;completed→20(置 status=1 发布
|
||||||
|
* + 短信后置);rejected→30;withdrawn→40;cc.sent→只落 notice_cc 抄送行(不动状态机,
|
||||||
|
* 2026-09-24)。行锁串行化并发推送,终态不回退守卫,终态 data 快照落 flow_result_json(详情页进度卡片)。
|
||||||
|
* <br>应答语义不变:本方法抛任何异常 → starter 回 5xx(flow-center 持续重试追平,免对账);
|
||||||
|
* 正常返回(含通知不存在/未绑流程/未知事件等不可重试场景)→ 200。
|
||||||
|
*
|
||||||
|
* <p><b>2026-10-17 T3 迁移:bean 已停用,代码原样保留作回退绳。</b>状态机推进改由
|
||||||
|
* {@link NoticeFlowBizAdapter} 声明、starter 的 {@code FlowBizCallbackBridge} 执行。
|
||||||
|
* 恢复本形态只需两步(语义与迁移前逐字节一致):① 给本类加回 {@code @Service}(连同其 import);
|
||||||
|
* ② 删除 yaml 的 {@code yh.flow-biz} 块——两者互斥(上下文已存在手写 FlowCallbackHandler 时
|
||||||
|
* starter 的桥不装配),故不会双写。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeFlowCallbackHandler implements FlowCallbackHandler {
|
||||||
|
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
private final com.yunhai.notice.service.sms.SmsSendService smsSendService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final com.yunhai.notice.repository.NoticeCcRepository noticeCcRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(FlowCallbackEvent event) {
|
||||||
|
Integer noticeId;
|
||||||
|
try {
|
||||||
|
noticeId = Integer.valueOf(event.getBizId().trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("[notice-flow] 回调 bizId 非法(丢弃):{}", event.getBizId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 行锁串行化:并发推送(flow-center 推送池)对同一通知的处理排队,防丢更新
|
||||||
|
Notice notice = noticeRepository.findByIdForUpdate(noticeId).orElse(null);
|
||||||
|
if (notice == null || !StringUtils.hasText(notice.getFlowGuid())) {
|
||||||
|
log.warn("[notice-flow] 回调通知不存在或未绑流程(丢弃):notice={}", event.getBizId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 单据级乱序闸门(starter 表级闸门之外的第二道:以行锁后读到的 notice 版本为准)
|
||||||
|
if (event.getInstanceVersion() != null && notice.getLastInstanceVersion() != null
|
||||||
|
&& event.getInstanceVersion() < notice.getLastInstanceVersion()) {
|
||||||
|
log.info("[notice-flow] 乱序事件丢弃:notice={} eventVersion={} last={}",
|
||||||
|
noticeId, event.getInstanceVersion(), notice.getLastInstanceVersion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
advance(notice, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态推进(幂等:同一事件重放由 eventId 去重拦截,此处只需正确覆盖目标态) */
|
||||||
|
private void advance(Notice notice, FlowCallbackEvent event) {
|
||||||
|
String code = event.getEvent();
|
||||||
|
Integer instanceVersion = event.getInstanceVersion();
|
||||||
|
Integer target;
|
||||||
|
switch (code) {
|
||||||
|
case "flow.cc.sent":
|
||||||
|
recordCc(notice, event);
|
||||||
|
return;
|
||||||
|
case "flow.completed":
|
||||||
|
target = FlowAuditStatusEnum.APPROVED.getCode();
|
||||||
|
break;
|
||||||
|
case "flow.rejected":
|
||||||
|
target = FlowAuditStatusEnum.REJECTED.getCode();
|
||||||
|
break;
|
||||||
|
case "flow.withdrawn":
|
||||||
|
target = FlowAuditStatusEnum.WITHDRAWN.getCode();
|
||||||
|
break;
|
||||||
|
case "flow.submitted":
|
||||||
|
case "flow.task.created":
|
||||||
|
case "flow.task.handled":
|
||||||
|
target = FlowAuditStatusEnum.APPROVING.getCode();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("[notice-flow] 未知事件(丢弃):{}", code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// P1-3 终态不回退守卫:终态事件与最后一个 task.handled 同 instanceVersion,若 handled
|
||||||
|
// 在 completed/rejected 之后落库(推送池并发不保序),无守卫会把 20/30 打回 10 并覆盖结果。
|
||||||
|
// R4 重提不冲突:编辑保存先本地置 audit=0,新一轮事件到达时行已非终态。
|
||||||
|
if (isTerminal(notice.getAuditStatus())) {
|
||||||
|
if (!isTerminal(target)) {
|
||||||
|
log.warn("[notice-flow] 终态不回退:notice={} audit={} 拒绝非终态事件 {}(v{})",
|
||||||
|
notice.getId(), notice.getAuditStatus(), code, instanceVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(notice.getFlowResultJson())) {
|
||||||
|
log.warn("[notice-flow] 终态结果已落定:notice={} audit={} 拒绝覆盖 flow_result_json(事件 {})",
|
||||||
|
notice.getId(), notice.getAuditStatus(), code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notice.setAuditStatus(target);
|
||||||
|
if (instanceVersion != null) {
|
||||||
|
notice.setLastInstanceVersion(instanceVersion);
|
||||||
|
}
|
||||||
|
// 终态明细快照(result/opinion/rejectNode,详情页进度卡片展示)
|
||||||
|
if (isTerminal(target)) {
|
||||||
|
notice.setFlowResultJson(snapshot(event));
|
||||||
|
}
|
||||||
|
boolean justPublished = false;
|
||||||
|
if (target == FlowAuditStatusEnum.APPROVED.getCode()) {
|
||||||
|
// 审核通过 = 正式发布:置 status=1(用户端自此可见)+ 发布后置(短信后移到此刻)
|
||||||
|
if (notice.getStatus() == null || notice.getStatus() != NoticeStatusEnum.PUBLISHED.getCode()) {
|
||||||
|
notice.setStatus(NoticeStatusEnum.PUBLISHED.getCode());
|
||||||
|
justPublished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noticeRepository.saveAndFlush(notice);
|
||||||
|
if (justPublished) {
|
||||||
|
sendSmsAfterPublish(notice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抄送留痕(2026-09-24,flow.cc.sent):逐人一条事件逐条落 notice_cc,供应用内
|
||||||
|
* 「抄送给我的」列表渲染、点击跳通知详情。不动审核状态机(抄送是知会不是审批,
|
||||||
|
* taskId 恒 null 也不落任务);已通过行锁与乱序闸门,重放由 eventId 幂等拦截。
|
||||||
|
* data 缺 ccUserId 属上游脏数据:记日志跳过(回 200),落库异常照旧外抛回 5xx 重试。
|
||||||
|
*/
|
||||||
|
private void recordCc(Notice notice, FlowCallbackEvent event) {
|
||||||
|
Map<String, Object> data = event.getData();
|
||||||
|
Object ccUserId = data == null ? null : data.get("ccUserId");
|
||||||
|
if (ccUserId == null || String.valueOf(ccUserId).isEmpty()) {
|
||||||
|
log.warn("[notice-flow] cc.sent 缺 ccUserId(丢弃):notice={} eventId={}",
|
||||||
|
notice.getId(), event.getEventId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.yunhai.notice.entity.NoticeCc cc = new com.yunhai.notice.entity.NoticeCc();
|
||||||
|
cc.setSchId(notice.getSchId());
|
||||||
|
cc.setNoticeId(notice.getId());
|
||||||
|
cc.setFlowInstanceId(event.getInstanceId());
|
||||||
|
cc.setNodeId(data.get("nodeId") == null ? null : String.valueOf(data.get("nodeId")));
|
||||||
|
cc.setNodeName(data.get("nodeName") == null ? null : String.valueOf(data.get("nodeName")));
|
||||||
|
cc.setCcUserId(Integer.valueOf(String.valueOf(ccUserId)));
|
||||||
|
cc.setCcUserName(data.get("ccUserName") == null ? null : String.valueOf(data.get("ccUserName")));
|
||||||
|
cc.setSentTime(LocalDateTime.now());
|
||||||
|
noticeCcRepository.save(cc);
|
||||||
|
log.info("[notice-flow] 抄送留痕:notice={} instance={} ccUser={} node={}",
|
||||||
|
notice.getId(), event.getInstanceId(), cc.getCcUserId(), cc.getNodeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String snapshot(FlowCallbackEvent event) { if (event.getData() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(event.getData());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[notice-flow] 终态快照序列化失败(置 null 不阻断推进):{}", e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核通过后的短信后置(绑流程通知 create 时不发,方案 §5.2 短信时机后移) */
|
||||||
|
private void sendSmsAfterPublish(Notice notice) {
|
||||||
|
if (notice.getSmsFlag() == null || notice.getSmsFlag() != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 与直发链路 trySendSms 的 isImmediatePublish 语义对齐:定时未到点的通知此刻不发
|
||||||
|
// (可见性已生效),到点由 SmsCatchupJob 补发(§3.2 修 BP-R5-2 短信永久丢失缺口)
|
||||||
|
if (notice.getTimingSendTime() != null
|
||||||
|
&& notice.getTimingSendTime().isAfter(LocalDateTime.now())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
smsSendService.sendOnce(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审批终态(20 通过 / 30 驳回 / 40 撤回)——终态行的状态与结果明细只允许被新一轮(本地先置 0)改写 */
|
||||||
|
private static boolean isTerminal(Integer auditStatus) {
|
||||||
|
return auditStatus != null
|
||||||
|
&& (auditStatus == FlowAuditStatusEnum.APPROVED.getCode()
|
||||||
|
|| auditStatus == FlowAuditStatusEnum.REJECTED.getCode()
|
||||||
|
|| auditStatus == FlowAuditStatusEnum.WITHDRAWN.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.openapi.flow.YschFlowOpenApi;
|
||||||
|
import com.yunhai.platform.openapi.flow.model.FlowEmbedUrl;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 办件进度嵌入页换址(2026-10-03 嵌入页方案):子应用 Drawer 内 iframe 直嵌 flow-center
|
||||||
|
* 统一进度页,零样式复制。与 Diagram 同口径的用户级守卫(未发布仅提交人/管理员/办理人
|
||||||
|
* /被抄送人可见);oapi 层只认 schId——"谁能看"必须落在本端点,应用签名不替代身份。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeFlowEmbedService {
|
||||||
|
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
private final NoticeFlowGuard noticeFlowGuard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取嵌入页地址(pageUrl 含 10 分钟短时 token):现取现发、不缓存、不落库、
|
||||||
|
* 不打进应用日志(token 泄露窗口即有效期,纪律见方案 §6.1/§7)。
|
||||||
|
*/
|
||||||
|
public Map<String, String> embedUrl(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = noticeRepository.findById(noticeId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "通知不存在: " + noticeId));
|
||||||
|
if (notice.getStatus() == null || notice.getStatus() != 1) {
|
||||||
|
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||||
|
}
|
||||||
|
if (notice.getFlowInstanceId() == null || notice.getSchId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该通知尚未进入流程");
|
||||||
|
}
|
||||||
|
FlowEmbedUrl embed = YschFlowOpenApi.embedUrl(
|
||||||
|
null, FlowCenterClient.BIZ_TYPE, String.valueOf(notice.getId()),
|
||||||
|
notice.getSchId(), FlowEmbedUrl.class);
|
||||||
|
if (embed == null || !StringUtils.hasText(embed.getPageUrl())) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "嵌入页地址获取失败,请稍后重试");
|
||||||
|
}
|
||||||
|
return Collections.singletonMap("pageUrl", embed.getPageUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未发布通知(status=0,审核载体)的详情放行守卫(方案 §5.5,V2.0 B.4#8 同构)。
|
||||||
|
* 四类人可见:提交人本人(publisherAccount)/ 本校管理员(type=1 或 roleId∈{2,5,7})/
|
||||||
|
* 该办件流程办理人(instance.assigneeIds 含当前用户,60s 短缓存——审批中途换人不实时,可接受)/
|
||||||
|
* 该办件被抄送人(notice_cc 有行,2026-09-24——抄送是知会,应用内「抄送给我的」点开必须可见)。
|
||||||
|
* 其余用户查询即"通知不存在或未发布"(查不到语义,不泄露存在性)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeFlowGuard {
|
||||||
|
|
||||||
|
/** 管理员角色白名单(校级管理员/分管领导等,与既有菜单权限体系对齐) */
|
||||||
|
private static final List<Integer> ADMIN_ROLE_IDS = Arrays.asList(2, 5, 7);
|
||||||
|
|
||||||
|
private static final long ASSIGNEE_CACHE_MS = 60_000L;
|
||||||
|
|
||||||
|
private final FlowClient flowClient;
|
||||||
|
private final NoticeFlowProperties properties;
|
||||||
|
private final com.yunhai.notice.repository.NoticeCcRepository noticeCcRepository;
|
||||||
|
|
||||||
|
private final Map<Long, CachedAssignees> assigneeCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static class CachedAssignees {
|
||||||
|
final List<Integer> userIds;
|
||||||
|
final long expireAt;
|
||||||
|
|
||||||
|
CachedAssignees(List<Integer> userIds) {
|
||||||
|
this.userIds = userIds;
|
||||||
|
this.expireAt = System.currentTimeMillis() + ASSIGNEE_CACHE_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前用户可查看未发布通知;不可见抛 FORBIDDEN(文案=查不到语义)。
|
||||||
|
* status=1 已发布通知不走本守卫(既有行为零变化)。
|
||||||
|
*/
|
||||||
|
public void requireUnpublishedVisible(Notice notice, BoaUser user) {
|
||||||
|
if (notice == null || user == null) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
// 1) 提交人本人
|
||||||
|
if (StringUtils.hasText(notice.getPublisherAccount())
|
||||||
|
&& notice.getPublisherAccount().equals(user.getAccount())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2) 本校管理员(type=1 校管 或 roleId 命中白名单)
|
||||||
|
if (isAdmin(user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 3) 该办件办理人(assigneeIds 含当前用户 id;flowInstanceId 缺失=尚未提交成功,仅前两类可见)
|
||||||
|
if (notice.getFlowInstanceId() != null && properties.isEnabled()
|
||||||
|
&& currentAssignees(notice).contains(user.getId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 4) 该办件被抄送人(notice_cc 本地表有行即放行——抄送发生即知会,无需查流程中心)
|
||||||
|
if (noticeCcRepository.existsByNoticeIdAndCcUserId(notice.getId(), user.getId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员校验(应用设置 > 流程绑定保存用):绑定决定全校"发全部用户是否需要审核",
|
||||||
|
* 与"谁能看未发布通知"同一口径({@link #ADMIN_ROLE_IDS})。
|
||||||
|
*/
|
||||||
|
public void requireAdmin(BoaUser user) {
|
||||||
|
if (user == null || !isAdmin(user)) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "仅本校管理员可修改审核流程绑定");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否本校管理员(口径同下方守卫;绑定页据此决定是否可编辑) */
|
||||||
|
public boolean isAdmin(BoaUser user) {
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (user.getType() != null && user.getType() == 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (user.getRoleIds() != null) {
|
||||||
|
for (Integer roleId : user.getRoleIds()) {
|
||||||
|
if (roleId != null && ADMIN_ROLE_IDS.contains(roleId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> currentAssignees(Notice notice) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
CachedAssignees cached = assigneeCache.get(notice.getFlowInstanceId());
|
||||||
|
if (cached != null && cached.expireAt > now) {
|
||||||
|
return cached.userIds == null ? java.util.Collections.emptyList() : cached.userIds;
|
||||||
|
}
|
||||||
|
List<Integer> assignees = null;
|
||||||
|
try {
|
||||||
|
FlowModels.InstanceVo instance = flowClient.getInstanceByBiz(
|
||||||
|
notice.getSchId(), FlowCenterClient.BIZ_TYPE, String.valueOf(notice.getId()));
|
||||||
|
if (instance != null) {
|
||||||
|
assignees = instance.getAssigneeIds();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 流程中心不可达:fail-closed(不放行),日志留痕
|
||||||
|
log.warn("[notice-flow] 办理人查询失败(fail-closed):notice={} {}", notice.getId(), e.toString());
|
||||||
|
}
|
||||||
|
assigneeCache.put(notice.getFlowInstanceId(),
|
||||||
|
new CachedAssignees(assignees == null ? java.util.Collections.emptyList() : assignees));
|
||||||
|
return assignees == null ? java.util.Collections.emptyList() : assignees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BizException notFound() {
|
||||||
|
return new BizException(ErrorCode.FORBIDDEN, "通知不存在或未发布");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知-流程中心接缝配置(notice.flow.*)。front-base-url 已删(2026-09-26):
|
||||||
|
* bizUrl 改传裸相对路径,绝对化由接收方按页面归属承担(自指地址优化 Phase 2)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "notice.flow")
|
||||||
|
public class NoticeFlowProperties {
|
||||||
|
|
||||||
|
/** 是否接入流程中心(false=编辑页选择器隐藏、FlowClient 全部抛业务异常降级) */
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* flow-center 基址的 <b>legacy 回落</b>(Tier 0 ad-hoc TargetSpec 路径)。
|
||||||
|
* 主路径是服务目录 {@code yh.open.services.flowcenter} → {@code YschFlowOpenApi} 门面;
|
||||||
|
* 本键仅迁移期兼容,<b>不再给默认值</b>——两者都空时 {@code FlowCenterClient} 构造期
|
||||||
|
* fail-fast,避免静默打 localhost 把"地址没配"伪装成"流程中心不可达"。
|
||||||
|
*/
|
||||||
|
private String baseUrl = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package com.yunhai.notice.service.flow;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.entity.NoticeFlowBinding;
|
||||||
|
import com.yunhai.notice.entity.UpFile;
|
||||||
|
import com.yunhai.notice.enums.FlowAuditStatusEnum;
|
||||||
|
import com.yunhai.notice.model.flow.FlowModels;
|
||||||
|
import com.yunhai.notice.repository.NoticeFlowBindingRepository;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.notice.repository.UpFileRepository;
|
||||||
|
import com.yunhai.notice.utils.ReceiveParser;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.auth.spring.CurrentBoaUser;
|
||||||
|
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||||
|
import com.yunhai.platform.yschflow.FlowApplySpec;
|
||||||
|
import com.yunhai.platform.yschflow.FlowSubmitTemplate;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知-流程中心接合服务:提交/重提(D9 AFTER_COMMIT 出站)、回调注册管理、进度组装。
|
||||||
|
* 提交语义(方案 §5.2):apply 幂等(bizId=notice.id),失败保持 0 待提交可重推;
|
||||||
|
* 10 审批中禁止重复提交/编辑/删除(删除需先撤回)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoticeFlowService {
|
||||||
|
|
||||||
|
private final NoticeRepository noticeRepository;
|
||||||
|
private final NoticeFlowBindingRepository bindingRepository;
|
||||||
|
private final UpFileRepository upFileRepository;
|
||||||
|
private final FlowClient flowClient;
|
||||||
|
private final NoticeFlowProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final CurrentBoaUser currentBoaUser;
|
||||||
|
private final NoticeFlowGuard noticeFlowGuard;
|
||||||
|
private final FlowSubmitTemplate flowSubmitTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交审核(D9 AFTER_COMMIT 出站,编排纪律交 {@link FlowSubmitTemplate},2026-10-17 T3 迁移):
|
||||||
|
* 出站动作在事务提交后的独立线程执行,提交人身份与单据快照在请求线程先取好。
|
||||||
|
* <p>E2E 实测教训(迁移前本类自带线程池,语义不变):afterCommit 回调若留在原请求线程执行,
|
||||||
|
* OSIV 绑定的 EntityManager 已随提交关闭,后续 repository 写会话静默丢失(有成功日志但库无行);
|
||||||
|
* 「能不能提交」的守卫也必须在提交后的状态上判定,故随 spec 的 preflight 一起搬到出站线程。</p>
|
||||||
|
*/
|
||||||
|
public void submitAfterCommit(Integer noticeId) {
|
||||||
|
final BoaUser user = currentUser();
|
||||||
|
final Notice notice = requireNotice(noticeId);
|
||||||
|
flowSubmitTemplate.submitAfterCommit(noticeId,
|
||||||
|
specOf(notice, user).preflight(() -> checkSubmittable(notice, user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交/重提审核(幂等入口:编辑页重提按钮);失败当场抛业务异常给调用方 */
|
||||||
|
public void trySubmitFlow(Integer noticeId) {
|
||||||
|
trySubmitFlow(noticeId, currentUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交/重提(带访问者——控制器直传登录人,BP-R5-3 守卫入口) */
|
||||||
|
public void trySubmitFlow(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = requireNotice(noticeId);
|
||||||
|
checkSubmittable(notice, user);
|
||||||
|
flowSubmitTemplate.submit(noticeId, specOf(notice, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交前置校验(同步入口当场抛、异步入口由 spec.preflight 在出站线程抛后静默保持待提交):
|
||||||
|
* BP-R5-3 三分支守卫 + 未绑流程 + 审批中禁止重复提交。
|
||||||
|
*/
|
||||||
|
private void checkSubmittable(Notice notice, BoaUser user) {
|
||||||
|
// BP-R5-3:Trace/TrySubmit 原先仅验存在性,任意登录用户可对他人通知触发重提——补
|
||||||
|
// NoticeFlowGuard 三分支(提交人/管理员/办理人),与 Detail 的守卫口径对齐
|
||||||
|
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||||
|
if (!StringUtils.hasText(notice.getFlowGuid())) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该通知未绑定审核流程");
|
||||||
|
}
|
||||||
|
Integer audit = notice.getAuditStatus();
|
||||||
|
if (audit != null && audit == FlowAuditStatusEnum.APPROVING.getCode()) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "审批中,不可重复提交,请先撤回");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起编排入参(契约冻结:bizId=notice.id、bizType=notice;bizUrl 不再手工传,
|
||||||
|
* 由 starter 按 {@code yh.flow-biz.detail-route} 拼装,产物与迁移前的
|
||||||
|
* {@code JumpTarget.page("list/detail").param("serid", id)} 逐字节相同)。
|
||||||
|
* 标题/表单/快照都取自请求线程读到的已提交实体值,出站前不落库。
|
||||||
|
*/
|
||||||
|
private FlowApplySpec specOf(Notice notice, BoaUser user) {
|
||||||
|
return FlowApplySpec.of(String.valueOf(notice.getId()), notice.getFlowGuid(),
|
||||||
|
() -> notice.getTitle(), () -> buildFormData(notice), () -> buildSnapshot(notice))
|
||||||
|
.bizType(FlowCenterClient.BIZ_TYPE)
|
||||||
|
.schId(notice.getSchId() == null ? null : Long.valueOf(notice.getSchId()))
|
||||||
|
.submitter(user == null ? null : user.getId(),
|
||||||
|
user == null ? null : (StringUtils.hasText(user.getName()) ? user.getName() : user.getAccount()))
|
||||||
|
// E2E 实测教训:apply 出站期间回调可能已并发推进,回填必须是窄列原子 UPDATE,
|
||||||
|
// 且 audit=20(已通过)不回退——守卫在 markApproving 的 case-when 里
|
||||||
|
.onApplied((instanceId, duplicated) -> noticeRepository.markApproving(notice.getId(), instanceId))
|
||||||
|
.onSubmitError((bizKey, error) -> log.warn("[notice-flow] 提交审核失败(保持待提交,可重推):"
|
||||||
|
+ "notice={} {}", bizKey, String.valueOf(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** formData 键与变量契约同源(yh.flow-biz.variables=[deptId, deptName],发送前由 starter 本地预检) */
|
||||||
|
private Map<String, Object> buildFormData(Notice notice) {
|
||||||
|
Map<String, Object> formData = new LinkedHashMap<>();
|
||||||
|
formData.put("deptId", notice.getDeptId());
|
||||||
|
formData.put("deptName", notice.getDeptName());
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务摘要:原键照旧(流程中心透传、嵌入页不展示)+ 约定键 summaryFields(嵌入页「业务信息」唯一渲染源) */
|
||||||
|
private Map<String, Object> buildSnapshot(Notice notice) {
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||||
|
snapshot.put("title", notice.getTitle());
|
||||||
|
snapshot.put("summary", plainSummary(notice.getContent()));
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (UpFile f : upFileRepository.findByTopicTypeAndTopicId("notice", notice.getId())) {
|
||||||
|
if (StringUtils.hasText(f.getFileTrueName())) {
|
||||||
|
names.add(f.getFileTrueName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot.put("attachmentNames", names);
|
||||||
|
snapshot.put("summaryFields", buildSummaryFields(notice));
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 嵌入页「业务信息」三行:通知标题 / 来源部门 / 通知对象 */
|
||||||
|
private List<Map<String, Object>> buildSummaryFields(Notice notice) {
|
||||||
|
List<Map<String, Object>> fields = new ArrayList<>();
|
||||||
|
fields.add(summaryField("通知标题", notice.getTitle()));
|
||||||
|
fields.add(summaryField("来源部门", notice.getDeptName()));
|
||||||
|
fields.add(summaryField("通知对象", buildReceiveSummary(notice)));
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> summaryField(String label, Object value) {
|
||||||
|
Map<String, Object> field = new LinkedHashMap<>();
|
||||||
|
field.put("label", label);
|
||||||
|
field.put("value", value == null ? "" : value);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知对象展示串:全员(receive_list 空 / ALL / UserAll)⇒ "全部";指定 ⇒ 前 3 个姓名以「、」连接,
|
||||||
|
* 多于 3 人再追 " 等N人"(如 "张三、李四、王五 等11人")。
|
||||||
|
* 姓名经用户中心解析(手法同 NoticeService.getReadDetailGrouped),失败回落原 id/账号;
|
||||||
|
* 只解析前 3 个——本方法在 apply 提交时执行,避免 N 次远程调用。
|
||||||
|
*/
|
||||||
|
private String buildReceiveSummary(Notice notice) {
|
||||||
|
List<String> accounts = ReceiveParser.parse(notice.getReceiveList());
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
return "全部";
|
||||||
|
}
|
||||||
|
int total = accounts.size();
|
||||||
|
List<String> shown = new ArrayList<>();
|
||||||
|
for (String account : accounts.subList(0, Math.min(3, total))) {
|
||||||
|
shown.add(resolveReceiverName(account));
|
||||||
|
}
|
||||||
|
String joined = String.join("、", shown);
|
||||||
|
return total > 3 ? joined + " 等" + total + "人" : joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接收明细可能存登录名,也可能是历史数字 userId:先按登录名查,查不到且纯数字再按 id 查 */
|
||||||
|
private String resolveReceiverName(String account) {
|
||||||
|
try {
|
||||||
|
BoaUser user = YschBaseOpenApi.getUserInfo(account);
|
||||||
|
if (user == null && StringUtils.hasText(account) && account.chars().allMatch(Character::isDigit)) {
|
||||||
|
user = YschBaseOpenApi.getUserInfo(Integer.parseInt(account));
|
||||||
|
}
|
||||||
|
return user != null && StringUtils.hasText(user.getName()) ? user.getName() : account;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[notice-flow] 通知对象姓名解析失败 account={}", account, e);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 富文本剥标签取纯文本摘要(截断 300 字) */
|
||||||
|
private String plainSummary(String content) {
|
||||||
|
if (!StringUtils.hasText(content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String text = content.replaceAll("<[^>]+>", "").replaceAll(" ", " ").trim();
|
||||||
|
return text.length() > 300 ? text.substring(0, 300) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进度组装(trace 接口):auditStatus/status/version/trace[]/currentNodes/nodeSummary;未绑流程返回未接入语义 */
|
||||||
|
public Map<String, Object> buildTrace(Integer noticeId) {
|
||||||
|
return buildTrace(noticeId, currentUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度组装(带访问者,BP-R5-3):未发布通知(status=0,审核载体)仅提交人/管理员/办理人
|
||||||
|
* 可见轨迹与驳回意见(NoticeFlowGuard 三分支);已发布(status=1)内容本就公开,登录即可看。
|
||||||
|
*/
|
||||||
|
public Map<String, Object> buildTrace(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = requireNotice(noticeId);
|
||||||
|
if (notice.getStatus() == null || notice.getStatus() != 1) {
|
||||||
|
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("bound", StringUtils.hasText(notice.getFlowGuid()));
|
||||||
|
result.put("flowGuid", notice.getFlowGuid());
|
||||||
|
result.put("flowInstanceId", notice.getFlowInstanceId());
|
||||||
|
result.put("auditStatus", notice.getAuditStatus());
|
||||||
|
result.put("auditStatusDesc", FlowAuditStatusEnum.descOf(notice.getAuditStatus()));
|
||||||
|
result.put("status", notice.getStatus());
|
||||||
|
if (!StringUtils.hasText(notice.getFlowGuid()) || notice.getAuditStatus() == null) {
|
||||||
|
return result; // 未接入流程中心(编辑页/详情页据此隐藏进度卡片)
|
||||||
|
}
|
||||||
|
result.put("flowResult", parseJson(notice.getFlowResultJson()));
|
||||||
|
if (properties.isEnabled()) {
|
||||||
|
try {
|
||||||
|
FlowModels.InstanceVo instance = flowClient.getInstanceByBiz(
|
||||||
|
notice.getSchId(), FlowCenterClient.BIZ_TYPE, String.valueOf(notice.getId()));
|
||||||
|
if (instance != null) {
|
||||||
|
result.put("instanceStatus", instance.getStatus());
|
||||||
|
result.put("version", instance.getVersion());
|
||||||
|
result.put("nodeSummary", instance.getNodeSummary());
|
||||||
|
result.put("currentNodes", instance.getCurrentNodes());
|
||||||
|
result.put("assigneeIds", instance.getAssigneeIds());
|
||||||
|
result.put("submitTime", instance.getSubmitTime());
|
||||||
|
result.put("finishTime", instance.getFinishTime());
|
||||||
|
result.put("trace", parseJson(instance.getTraceJson()));
|
||||||
|
}
|
||||||
|
if (notice.getFlowInstanceId() != null) {
|
||||||
|
// 结构化步骤(steps[0] 固定"发起申请",同节点多任务每人一行)——
|
||||||
|
// 2026-09-17 接入 SDK progress,详情页时间线数据源;失败降级不阻断
|
||||||
|
FlowModels.ProgressVo progress =
|
||||||
|
flowClient.progress(notice.getSchId(), notice.getFlowInstanceId());
|
||||||
|
if (progress != null) {
|
||||||
|
result.put("steps", progress.getSteps());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 进度查询失败不阻断详情:展示本地镜像态即可(流程中心停机时详情仍可用)
|
||||||
|
log.warn("[notice-flow] 进度查询失败:notice={} {}", noticeId, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回审核(2026-09-17 治理:原先前端直调 flow-center 用户面——跨应用 CAS 票据 + CORS
|
||||||
|
* 双重外溢,且"谁能撤"落在前端)。现经 SDK 托管件后端中转:①本人(publisherAccount)
|
||||||
|
* = 权威授权;②审批中(10) 且已有实例;③对端再按 submitterId 复核一次。
|
||||||
|
* 撤回后镜像状态由 flow.withdrawn 回调推进(40),本方法不改本地状态——单一写入口。
|
||||||
|
*/
|
||||||
|
public void withdrawNotice(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = requireNotice(noticeId);
|
||||||
|
if (user == null || !StringUtils.hasText(notice.getPublisherAccount())
|
||||||
|
|| !notice.getPublisherAccount().equals(user.getAccount())) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "仅通知创建人可撤回审核");
|
||||||
|
}
|
||||||
|
Integer audit = notice.getAuditStatus();
|
||||||
|
if (audit == null || audit != FlowAuditStatusEnum.APPROVING.getCode()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的通知可撤回");
|
||||||
|
}
|
||||||
|
if (notice.getFlowInstanceId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该通知尚未进入流程,无需撤回");
|
||||||
|
}
|
||||||
|
flowClient.withdraw(notice.getSchId(), notice.getFlowInstanceId(), user.getId());
|
||||||
|
log.info("[notice-flow] 撤回审核:notice={} instance={} user={}",
|
||||||
|
noticeId, notice.getFlowInstanceId(), user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程示意图(PNG 流,同源直嵌 {@code <img>}):可见性与 Trace 同口径(未发布仅三类人)。
|
||||||
|
* token 与字节都在服务端流转,前端不再需要知道 flow-center 地址。
|
||||||
|
*/
|
||||||
|
public byte[] diagramImage(Integer noticeId, BoaUser user) {
|
||||||
|
Notice notice = requireNotice(noticeId);
|
||||||
|
if (notice.getStatus() == null || notice.getStatus() != 1) {
|
||||||
|
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(notice.getFlowGuid()) || notice.getAuditStatus() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该通知未接入审核流程");
|
||||||
|
}
|
||||||
|
if (notice.getFlowInstanceId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "该通知尚未进入流程,无流程图");
|
||||||
|
}
|
||||||
|
return flowClient.diagramImage(notice.getSchId(), notice.getFlowInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑页"审核流程"选择器数据源(未启用返回空列表由前端隐藏) */
|
||||||
|
public List<FlowModels.FlowSummaryVo> options(Integer schId) {
|
||||||
|
if (!properties.isEnabled()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return flowClient.listFlows(schId);
|
||||||
|
} catch (BizException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[notice-flow] 流程列表查询失败:{}", e.toString());
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "审核流程列表获取失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoaUser currentUser() {
|
||||||
|
return currentBoaUser == null ? null : currentBoaUser.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 应用级流程绑定(应用设置 > 流程绑定) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本校绑定视图(设置页展示 / 发通知页按钮显隐):bound / flowGuid / flowName / flowEnabled / canEdit。
|
||||||
|
* 无学校身份或未绑定 → bound=false。读取不需要管理员(发通知页人人都要据此判断是否走审核)。
|
||||||
|
*/
|
||||||
|
public Map<String, Object> bindingView(Integer schId, BoaUser user) {
|
||||||
|
Map<String, Object> view = new LinkedHashMap<>();
|
||||||
|
NoticeFlowBinding binding = schId == null ? null : bindingRepository.findBySchId(schId).orElse(null);
|
||||||
|
boolean bound = binding != null && StringUtils.hasText(binding.getFlowGuid());
|
||||||
|
view.put("bound", bound);
|
||||||
|
view.put("flowGuid", bound ? binding.getFlowGuid() : null);
|
||||||
|
view.put("flowName", bound ? binding.getFlowName() : null);
|
||||||
|
view.put("flowEnabled", properties.isEnabled());
|
||||||
|
view.put("canEdit", noticeFlowGuard.isAdmin(user));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发通知分流唯一判定入口:本校已绑定且流程中心可用时返回绑定 GUID;否则 null(走直发)。
|
||||||
|
* 流程中心未接入(properties.enabled=false)时不进审核轨道——否则通知会永远停在未发布态。
|
||||||
|
*/
|
||||||
|
public String boundFlowGuid(Integer schId) {
|
||||||
|
if (schId == null || !properties.isEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
NoticeFlowBinding binding = bindingRepository.findBySchId(schId).orElse(null);
|
||||||
|
if (binding == null || !StringUtils.hasText(binding.getFlowGuid())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return binding.getFlowGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存绑定(空 flowGuid=解绑,直接删行)。绑定的流程必须是流程中心"已发布"流程——
|
||||||
|
* 值由 options() 同一数据源校验,避免手改请求把不存在的 GUID 落库(发通知时才发现)。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> saveBinding(BoaUser user, String flowGuid) {
|
||||||
|
noticeFlowGuard.requireAdmin(user);
|
||||||
|
Integer schId = currentSchId(user);
|
||||||
|
NoticeFlowBinding binding = bindingRepository.findBySchId(schId).orElse(null);
|
||||||
|
if (!StringUtils.hasText(flowGuid)) {
|
||||||
|
if (binding != null) {
|
||||||
|
bindingRepository.delete(binding);
|
||||||
|
log.info("[notice-flow] 解除流程绑定:schId={} 原流程={}", schId, binding.getFlowGuid());
|
||||||
|
}
|
||||||
|
return bindingView(schId, user);
|
||||||
|
}
|
||||||
|
String name = requirePublishedFlowName(schId, flowGuid.trim());
|
||||||
|
if (binding == null) {
|
||||||
|
binding = new NoticeFlowBinding();
|
||||||
|
binding.setSchId(schId);
|
||||||
|
}
|
||||||
|
binding.setFlowGuid(flowGuid.trim());
|
||||||
|
binding.setFlowName(name);
|
||||||
|
bindingRepository.save(binding);
|
||||||
|
log.info("[notice-flow] 保存流程绑定:schId={} flowGuid={} name={}", schId, binding.getFlowGuid(), name);
|
||||||
|
return bindingView(schId, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验流程 GUID 属于本校已发布流程并返回流程名(流程中心不可达时抛业务异常,不落脏绑定) */
|
||||||
|
private String requirePublishedFlowName(Integer schId, String flowGuid) {
|
||||||
|
for (FlowModels.FlowSummaryVo flow : options(schId)) {
|
||||||
|
if (flowGuid.equals(flow.getFlowGuid())) {
|
||||||
|
return flow.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程不存在或未发布:" + flowGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer currentSchId(BoaUser user) {
|
||||||
|
if (user == null || user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "无法确定当前学校身份,请重新登录后再试");
|
||||||
|
}
|
||||||
|
return user.getCurrentRole().getSchId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notice requireNotice(Integer noticeId) {
|
||||||
|
return noticeRepository.findById(noticeId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "通知不存在: " + noticeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode parseJson(String json) { if (!StringUtils.hasText(json)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.yunhai.notice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.notice.model.SchTeaList;
|
||||||
|
import com.yunhai.notice.service.IClassService;
|
||||||
|
import com.yunhai.platform.openapi.clazz.YschClassOpenApi;
|
||||||
|
import com.yunhai.platform.openapi.clazz.model.SchTea;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 班级管理(userclass)开放接口实现。<br>
|
||||||
|
* 2026-09-16 SDK 重构起改为委托类型化门面 {@link YschClassOpenApi}(服务目录 key=userclass:
|
||||||
|
* services/枚举兜底统一解析,签名/信封/错误语义由 SDK 承载),替代原手搓 RestTemplate +
|
||||||
|
* legacy 键 {@code yh.open.userclass} 直连(无签名旁路)。Notice 侧模型 {@link SchTeaList}
|
||||||
|
* 保留(消费方签名零改动),此处仅做字段映射。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ClassService implements IClassService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ClassService.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchTeaList> getSchTeaList(int schId) {
|
||||||
|
List<SchTea> teachers = YschClassOpenApi.getSchTeaList(schId);
|
||||||
|
log.info("[getSchTeaList] schId={}, 获取教师 {} 人", schId, teachers == null ? 0 : teachers.size());
|
||||||
|
if (teachers == null || teachers.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<SchTeaList> result = new ArrayList<>(teachers.size());
|
||||||
|
for (SchTea tea : teachers) {
|
||||||
|
result.add(map(tea));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SchTeaList map(SchTea tea) {
|
||||||
|
SchTeaList t = new SchTeaList();
|
||||||
|
t.setSchSerID(tea.getSchSerId());
|
||||||
|
t.setTeaTel(tea.getTeaTel());
|
||||||
|
t.setTeaName(tea.getTeaName());
|
||||||
|
t.setUserTeaSerID(tea.getUserTeaSerId());
|
||||||
|
t.setTeaIDCard(tea.getTeaIdCard());
|
||||||
|
t.setTeaNo(tea.getTeaNo());
|
||||||
|
t.setGroupIds(tea.getGroupIds());
|
||||||
|
t.setGroupId(tea.getGroupId());
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
package com.yunhai.notice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.ContactsGroup;
|
||||||
|
import com.yunhai.notice.entity.ContactsGroupUser;
|
||||||
|
import com.yunhai.notice.repository.ContactsGroupRepository;
|
||||||
|
import com.yunhai.notice.repository.ContactsGroupUserRepository;
|
||||||
|
import com.yunhai.notice.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.base.YschBaseOpenApi;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.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.notice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.Notice;
|
||||||
|
import com.yunhai.notice.entity.SysDict;
|
||||||
|
import com.yunhai.notice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.notice.repository.SysDictRepository;
|
||||||
|
import com.yunhai.notice.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,344 @@
|
|||||||
|
package com.yunhai.notice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.UpFile;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileVo;
|
||||||
|
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||||
|
import com.yunhai.notice.repository.UpFileRepository;
|
||||||
|
import com.yunhai.notice.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件存储根目录(生产容器内 /data/upload,对应 projman 卷 /data/uploads/notice)。
|
||||||
|
* 平台标准(与 userhome/userbase/flow-center 同款):FILE_PATH 各子应用值相同
|
||||||
|
* (/data/upload),统一收敛在共享配置 application-platform.yml 一份,子应用不再各自维护;
|
||||||
|
* notice 曾走过渡键 FILE_UPLOAD_PATH(prod 配置 2026-10-10 已加,保留兼容);
|
||||||
|
* file.upload-path(application.yml 的 D:/data/upload)仅本地 Windows 开发用。
|
||||||
|
* 教训:生产曾因键名不匹配回落到 D:/data/upload——Linux 锚定为 /app/D:/data/upload
|
||||||
|
* 写进容器可写层,每次重新部署文件即丢(2026-10 事故)。
|
||||||
|
*/
|
||||||
|
@Value("${FILE_PATH:${FILE_UPLOAD_PATH:${file.upload-path:/data/upload}}}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
|
private static final String DEFAULT_APP_TYPE = "notice";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析存储绝对路径。uploadPath 需以绝对路径配置(生产容器内 /data/upload);
|
||||||
|
* 若误配为相对路径,此处统一锚定到进程工作目录——否则 MultipartFile.transferTo
|
||||||
|
* 会把相对目标解析到 Tomcat multipart 临时目录,与后续读取(按工作目录解析)分裂。
|
||||||
|
*/
|
||||||
|
private Path resolvePath(String relative) {
|
||||||
|
return Paths.get(uploadPath, relative).toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 上传 ============================
|
||||||
|
|
||||||
|
@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 = resolvePath(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 = resolvePath(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 = resolvePath(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 = resolvePath(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.notice.service.impl;
|
||||||
|
|
||||||
|
import com.yunhai.notice.entity.SmsModule;
|
||||||
|
import com.yunhai.notice.entity.SmsSchoolConfig;
|
||||||
|
import com.yunhai.notice.repository.SmsModuleRepository;
|
||||||
|
import com.yunhai.notice.repository.SmsSchoolConfigRepository;
|
||||||
|
import com.yunhai.notice.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user