init local code
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,20 @@
|
||||
# Notice 后端镜像(基于 mvn package 产出的 target/notice.jar)
|
||||
# 构建上下文 = service 目录: docker build -f service/docker/Dockerfile -t <img> service
|
||||
FROM eclipse-temurin:8-jre
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl tzdata \
|
||||
&& ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY target/notice.jar /app/notice.jar
|
||||
# 镜像内 application-prod.yml 仅作「nacos 不可达时的兜底默认」;
|
||||
# 正常启动由 bootstrap.yml 从 nacos 拉取线上配置(group=NOTICE, dataId=notice.yaml)后覆盖。
|
||||
COPY docker/application-prod.yml /app/config/application-prod.yml
|
||||
COPY docker/run-app.sh /app/run-app.sh
|
||||
RUN chmod +x /app/run-app.sh && mkdir -p /data/upload
|
||||
|
||||
EXPOSE 8070
|
||||
ENTRYPOINT ["/app/run-app.sh"]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Notice 生产配置(线上以 nacos 的 notice.yaml 为准,本文件是 nacos 不可达时的兜底)
|
||||
# 真正上线请在 Nacos 新建 dataId=notice.yaml、group=NOTICE 的配置覆盖以下项
|
||||
# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
|
||||
server:
|
||||
port: 8070
|
||||
servlet:
|
||||
# 对外经 caddy 暴露为 http://base.yunhaixingtong.com/notice
|
||||
context-path: /notice
|
||||
|
||||
spring:
|
||||
mvc:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
jackson:
|
||||
time-zone: GMT+8
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
datasource:
|
||||
# 与 UserBase 共用同一台 mysql8;库名为 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_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,215 @@
|
||||
<?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>
|
||||
|
||||
<!-- 基础平台架包:认证(CAS/会话/SSO)+ 应用间签名鉴权 + 用户中心静态门面 + 统一响应 -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-auth-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-app-auth-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-openapi-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-api-std</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- CAS 客户端(浏览器 SSO 重定向 / ticket 校验 / getRemoteUser 包装)。
|
||||
版本 3.5.1 已在本地 .m2 仓库。 -->
|
||||
<dependency>
|
||||
<groupId>org.jasig.cas.client</groupId>
|
||||
<artifactId>cas-client-core</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Excel 导入导出(EasyExcel 3.x,注解驱动、流式写入内存友好) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 缓存(Spring Cache + Redis,字典等低频变更数据缓存) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<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>
|
||||
</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,22 @@
|
||||
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.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.yunhai.notice")
|
||||
@EnableTransactionManagement
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class NoticeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 开启 fastjson 安全模式
|
||||
ParserConfig.getGlobalInstance().setSafeMode(true);
|
||||
SpringApplication.run(NoticeApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.yunhai.notice.common;
|
||||
|
||||
import com.yunhai.platform.apistd.ApiResponse;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器:将业务异常转为统一响应 {@link ApiResponse}(code/msg/data)。
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(NoticeException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleNoticeException(NoticeException ex) {
|
||||
log.warn("[NoticeException] {}", ex.getMessage());
|
||||
String msg = ex.getMessage() == null ? "业务异常" : ex.getMessage();
|
||||
return ApiResponse.fail(5, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleValidException(MethodArgumentNotValidException ex) {
|
||||
String msg = ex.getBindingResult().getFieldError() != null
|
||||
? ex.getBindingResult().getFieldError().getDefaultMessage()
|
||||
: "参数校验失败";
|
||||
log.warn("[Validation] {}", msg);
|
||||
return ApiResponse.fail(ErrorCode.PARAM_ERROR, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleConstraintViolation(ConstraintViolationException ex) {
|
||||
log.warn("[ConstraintViolation] {}", ex.getMessage());
|
||||
return ApiResponse.fail(ErrorCode.PARAM_ERROR, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleException(Exception ex) {
|
||||
log.error("[UnhandledException] ", ex);
|
||||
String msg = ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage();
|
||||
return ApiResponse.fail(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "服务器内部错误:" + msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yunhai.notice.common;
|
||||
|
||||
/**
|
||||
* 业务异常。等价于 UserBase 的 com.bestsch.utils.DException,统一以 RuntimeException 抛出。
|
||||
*/
|
||||
public class NoticeException extends RuntimeException {
|
||||
|
||||
public NoticeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public NoticeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yunhai.notice.common.context;
|
||||
|
||||
/**
|
||||
* 当前登录用户上下文(ThreadLocal)。<br>
|
||||
* 由 {@link com.yunhai.notice.common.interceptor.CurrentUserInterceptor} 在请求入口写入,
|
||||
* 供 JPA 审计监听器与业务层取用发布人/读者身份。<br><br>
|
||||
* 身份取自 CAS 写入的 {@code request.getRemoteUser()},本地未接入 CAS 时回退到
|
||||
* application.yml 的 dev.default-user 或请求头 X-Login-Account,便于本地联调。
|
||||
*/
|
||||
public class NoticeContext {
|
||||
|
||||
private static final ThreadLocal<String> ACCOUNT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> NAME = new ThreadLocal<>();
|
||||
|
||||
public static void set(String account, String name) {
|
||||
ACCOUNT.set(account);
|
||||
NAME.set(name);
|
||||
}
|
||||
|
||||
public static String getAccount() {
|
||||
return ACCOUNT.get();
|
||||
}
|
||||
|
||||
public static String getName() {
|
||||
return NAME.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
ACCOUNT.remove();
|
||||
NAME.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yunhai.notice.common.event;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知发布事件。在通知状态从草稿变更为已发布时发布,
|
||||
* 由 {@code MessageService} 监听并在事务提交后异步推送消息(短信/App/角标等)。
|
||||
*/
|
||||
public class NoticePublishedEvent {
|
||||
|
||||
private final Integer noticeId;
|
||||
private final String title;
|
||||
private final List<String> receiverAccounts;
|
||||
|
||||
public NoticePublishedEvent(Integer noticeId, String title, List<String> receiverAccounts) {
|
||||
this.noticeId = noticeId;
|
||||
this.title = title;
|
||||
this.receiverAccounts = receiverAccounts;
|
||||
}
|
||||
|
||||
public Integer getNoticeId() {
|
||||
return noticeId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<String> getReceiverAccounts() {
|
||||
return receiverAccounts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.yunhai.notice.common.interceptor;
|
||||
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 当前登录用户拦截器:优先取 CAS 写入的 {@code getRemoteUser()},
|
||||
* 其次取请求头 X-Login-Account / X-Login-Name(本地联调用),最后回退到 dev.default-user。
|
||||
* 写入 {@link NoticeContext} 供业务层与 JPA 审计监听器使用。
|
||||
*/
|
||||
@Component
|
||||
public class CurrentUserInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Value("${dev.default-user.account:admin}")
|
||||
private String devAccount;
|
||||
|
||||
@Value("${dev.default-user.name:管理员}")
|
||||
private String devName;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler) {
|
||||
String account = request.getRemoteUser();
|
||||
String name = null;
|
||||
if (account == null) {
|
||||
account = request.getHeader("X-Login-Account");
|
||||
name = request.getHeader("X-Login-Name");
|
||||
}
|
||||
if (account == null) {
|
||||
account = devAccount;
|
||||
name = devName;
|
||||
}
|
||||
if (name == null) {
|
||||
name = account;
|
||||
}
|
||||
NoticeContext.set(account, name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
NoticeContext.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.notice.common.listener;
|
||||
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.entity.BaseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.PreUpdate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(登录账号)与时间戳。
|
||||
* 对应 {@link BaseEntity} 的 createBy / updateBy / createTime / updateTime 字段。
|
||||
*/
|
||||
@Component
|
||||
public class NoticeAuditListener {
|
||||
|
||||
@PrePersist
|
||||
public void prePersist(BaseEntity entity) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
String account = NoticeContext.getAccount();
|
||||
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 = NoticeContext.getAccount();
|
||||
if (account != null) {
|
||||
entity.setUpdateBy(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.yunhai.notice.config;
|
||||
|
||||
/**
|
||||
* 全局路径常量。与 UserBase 一致:API 统一挂在 /{platform}/ 之下(platform = api / manage / mobiapi)。
|
||||
*/
|
||||
public enum Config {
|
||||
Inst;
|
||||
|
||||
public static final String API_PATH = "/{platform}"; // manage / api / mobiapi,前面有/后面无/,拼接子路径时用"/"分隔
|
||||
public static final String OPEN_API_PATH = "/oapi";
|
||||
}
|
||||
@@ -0,0 +1,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,31 @@
|
||||
package com.yunhai.notice.config;
|
||||
|
||||
import com.yunhai.notice.common.interceptor.CurrentUserInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置:注册当前用户拦截器(写入 {@link com.yunhai.notice.common.context.NoticeContext})。
|
||||
* 放行 swagger / OpenAPI 文档与静态资源。
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private CurrentUserInterceptor currentUserInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(currentUserInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/swagger-ui.html",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
"/doc.html",
|
||||
"/webjars/**",
|
||||
"/favicon.ico");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 基础平台代理接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/base/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* <p>
|
||||
* 这些接口在 C# 原版中依赖用户中心 OpenAPI(BaseOpenApi/HttpUserApi)获取数据,
|
||||
* Java 版暂以桩实现返回,后续接入平台用户中心 API 后补全。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
public class BaseApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BaseApiController.class);
|
||||
|
||||
@Value("${notice.app-name:通知公告}")
|
||||
private String appName;
|
||||
|
||||
// ============================ GetLayout ============================
|
||||
|
||||
/**
|
||||
* 获取布局数据(模块+头部)。
|
||||
* 前端: GET base/GetLayout
|
||||
* C# 通过 OpenAPI 获取用户模块权限列表。
|
||||
* Java 版返回基础布局信息,pathList 待接入用户中心后补全。
|
||||
*/
|
||||
@GetMapping("GetLayout")
|
||||
public Map<String, Object> getLayout() {
|
||||
String account = NoticeContext.getAccount();
|
||||
String name = NoticeContext.getName();
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("ssoUrl", "");
|
||||
data.put("username", name != null ? name : (account != null ? account : ""));
|
||||
data.put("pathList", Collections.emptyList()); // 待接入用户中心API
|
||||
data.put("topName", appName);
|
||||
data.put("pageUrl", "");
|
||||
data.put("logoUrl", "");
|
||||
data.put("initUrl", "");
|
||||
data.put("id", 0);
|
||||
data.put("avatar", "");
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================ GetSchoolAll ============================
|
||||
|
||||
/**
|
||||
* 获取当前用户学校组及用户(树形结构)。
|
||||
* 前端: GET base/GetSchoolAll?type=
|
||||
* C# 通过 ISchGroupBLL + ITeainfoBLL 获取学校组+教师树。
|
||||
* Java 版待接入用户中心API,暂返回空树。
|
||||
*/
|
||||
@GetMapping("GetSchoolAll")
|
||||
public List<Map<String, Object>> getSchoolAll(@RequestParam(value = "type", defaultValue = "") String type) {
|
||||
log.info("[getSchoolAll] 暂未接入用户中心API,返回空列表");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ GetGroupUser ============================
|
||||
|
||||
/**
|
||||
* 获取当前组用户。
|
||||
* 前端: GET base/GetGroupUser?gid=&role=
|
||||
* C# 通过 ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo 获取组内教师+管理员。
|
||||
* Java 版待接入用户中心API,暂返回空列表。
|
||||
*/
|
||||
@GetMapping("GetGroupUser")
|
||||
public List<Map<String, Object>> getGroupUser(
|
||||
@RequestParam("gid") Integer gid,
|
||||
@RequestParam(value = "role", defaultValue = "2") Integer role) {
|
||||
log.info("[getGroupUser] gid={}, 暂未接入用户中心API,返回空列表", gid);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ Logout ============================
|
||||
|
||||
/**
|
||||
* 退出登录。
|
||||
* 前端: POST base/Logout
|
||||
* C# 清除 Session + FormsAuth。
|
||||
* Java 版由 Spring Security/平台网关处理认证,此处仅返回成功。
|
||||
*/
|
||||
@PostMapping("Logout")
|
||||
public void logout() {
|
||||
// 认证由平台网关处理,此处无需额外操作
|
||||
log.info("[logout] 用户退出登录: {}", NoticeContext.getAccount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.ContactsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 联系组接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/contacts/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/contacts/")
|
||||
public class ContactsController {
|
||||
|
||||
@Autowired
|
||||
private ContactsService contactsService;
|
||||
|
||||
// ============================ AddGroup ============================
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
* 前端: POST contacts/AddGroup (form-urlencoded: name, type)
|
||||
*/
|
||||
@PostMapping("AddGroup")
|
||||
public void addGroup(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
|
||||
contactsService.addGroup(name, Integer.parseInt(type));
|
||||
}
|
||||
|
||||
// ============================ GetGroup ============================
|
||||
|
||||
/**
|
||||
* 获取联系组列表。
|
||||
* 前端: GET contacts/GetGroup?page=&count=&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);
|
||||
}
|
||||
|
||||
// ============================ EditGroup ============================
|
||||
|
||||
/**
|
||||
* 编辑联系组。
|
||||
* 前端: POST contacts/EditGroup (form-urlencoded: name, id, 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));
|
||||
}
|
||||
|
||||
// ============================ DelGroup ============================
|
||||
|
||||
/**
|
||||
* 删除联系组(支持批量)。
|
||||
* 前端: POST contacts/DelGroup (form-urlencoded: ids=1,2,3)
|
||||
*/
|
||||
@PostMapping("DelGroup")
|
||||
public void delGroup(@RequestParam("ids") String ids) {
|
||||
contactsService.delGroup(ids);
|
||||
}
|
||||
|
||||
// ============================ SetContactsTea ============================
|
||||
|
||||
/**
|
||||
* 设置联系组教师。
|
||||
* 前端: POST contacts/SetContactsTea (form-urlencoded: gid, ids)
|
||||
*/
|
||||
@PostMapping("SetContactsTea")
|
||||
public void setContactsTea(
|
||||
@RequestParam("gid") String gid,
|
||||
@RequestParam(value = "ids", required = false) String ids) {
|
||||
contactsService.setContactsTea(Integer.parseInt(gid), ids);
|
||||
}
|
||||
|
||||
// ============================ GetContactsTea ============================
|
||||
|
||||
/**
|
||||
* 获取联系组教师列表。
|
||||
* 前端: GET contacts/GetContactsTea?gid=&page=&count=
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
/**
|
||||
* 获取联系组树。
|
||||
* 前端: GET contacts/GetGroupTree?type=
|
||||
*/
|
||||
@GetMapping("GetGroupTree")
|
||||
public List<Map<String, Object>> getGroupTree(@RequestParam("type") Integer type) {
|
||||
return contactsService.getGroupTree(type);
|
||||
}
|
||||
|
||||
// ============================ GetDeptRoleGroupTree ============================
|
||||
|
||||
/**
|
||||
* 获取部门角色组树。
|
||||
* 前端: GET contacts/GetDeptRoleGroupTree
|
||||
*/
|
||||
@GetMapping("GetDeptRoleGroupTree")
|
||||
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||
return contactsService.getDeptRoleGroupTree();
|
||||
}
|
||||
|
||||
// ============================ Import ============================
|
||||
|
||||
/**
|
||||
* 导入联系组。
|
||||
* 前端: POST contacts/Import (Excel文件上传)
|
||||
*/
|
||||
@PostMapping("Import")
|
||||
public Map<String, Object> importContacts() {
|
||||
return contactsService.importContacts();
|
||||
}
|
||||
|
||||
// ============================ Export ============================
|
||||
|
||||
/**
|
||||
* 导出联系组。
|
||||
* 前端: GET contacts/Export
|
||||
*/
|
||||
@GetMapping("Export")
|
||||
public Map<String, Object> exportContacts() {
|
||||
return contactsService.exportContacts();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.DictService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 系统字典接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/base/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
public class DictController {
|
||||
|
||||
@Autowired
|
||||
private DictService dictService;
|
||||
|
||||
// ============================ GetSysDic ============================
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
* 前端: GET base/GetSysDic?Type=1
|
||||
*/
|
||||
@GetMapping("GetSysDic")
|
||||
public List<Map<String, Object>> getSysDic(@RequestParam(value = "type", defaultValue = "0") Integer type) {
|
||||
return dictService.getSysDic(type);
|
||||
}
|
||||
|
||||
// ============================ GetDictMangeList ============================
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表。
|
||||
* 前端: GET base/GetDictMangeList?type=dept
|
||||
*/
|
||||
@GetMapping("GetDictMangeList")
|
||||
public List<Map<String, Object>> getDictMangeList(@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
return dictService.getDictMangeList(type);
|
||||
}
|
||||
|
||||
// ============================ AddDeptDic ============================
|
||||
|
||||
/**
|
||||
* 添加发文单位。
|
||||
* 前端: POST base/AddDeptDic (form-urlencoded: Name)
|
||||
*/
|
||||
@PostMapping("AddDeptDic")
|
||||
public void addDeptDic(@RequestParam("name") String name) {
|
||||
dictService.addDeptDic(name);
|
||||
}
|
||||
|
||||
// ============================ UpdateDeptDic ============================
|
||||
|
||||
/**
|
||||
* 更新发文单位。
|
||||
* 前端: POST base/UpdateDeptDic (form-urlencoded: SerID, Name, Enable)
|
||||
*/
|
||||
@PostMapping("UpdateDeptDic")
|
||||
public void updateDeptDic(
|
||||
@RequestParam("SerID") Integer serID,
|
||||
@RequestParam("Name") String name,
|
||||
@RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable) {
|
||||
dictService.updateDeptDic(serID, name, enable);
|
||||
}
|
||||
|
||||
// ============================ DelDept ============================
|
||||
|
||||
/**
|
||||
* 删除发文单位。
|
||||
* 前端: GET base/DelDept?serid=xxx
|
||||
*/
|
||||
@GetMapping("DelDept")
|
||||
public void delDept(@RequestParam("serid") Integer serID) {
|
||||
dictService.delDept(serID);
|
||||
}
|
||||
|
||||
// ============================ OrdbyDicIds ============================
|
||||
|
||||
/**
|
||||
* 发文单位排序。
|
||||
* 前端: GET base/OrdbyDicIds?ids=1,2,3
|
||||
*/
|
||||
@GetMapping("OrdbyDicIds")
|
||||
public void ordbyDicIds(@RequestParam("ids") String ids) {
|
||||
dictService.ordbyDicIds(ids);
|
||||
}
|
||||
|
||||
// ============================ GetDeptSync ============================
|
||||
|
||||
/**
|
||||
* 发文单位同步。
|
||||
* 前端: GET base/GetDeptSync
|
||||
*/
|
||||
@GetMapping("GetDeptSync")
|
||||
public void getDeptSync() {
|
||||
dictService.getDeptSync();
|
||||
}
|
||||
|
||||
// ============================ GetSchModuleIsSms ============================
|
||||
|
||||
/**
|
||||
* 是否发送短信。
|
||||
* 前端: GET base/GetSchModuleIsSms
|
||||
*/
|
||||
@GetMapping("GetSchModuleIsSms")
|
||||
public Boolean getSchModuleIsSms() {
|
||||
return dictService.getSchModuleIsSms();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* 文件接口(兼容前端 Notice3.0 URL)。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
public class FileController {
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
/**
|
||||
* 附件上传(单文件)。
|
||||
* 前端: POST base/FileUpload (form-data, 含 AppType)
|
||||
* 返回: { fileid, host, fileurl, name, type, size }
|
||||
*/
|
||||
@PostMapping("FileUpload")
|
||||
public Map<String, Object> fileUpload(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "AppType", required = false) String appType) {
|
||||
UpFileVo vo = fileService.upload(file, appType != null ? appType : "notice", null);
|
||||
return upFileToResult(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(多文件,返回数组)。
|
||||
* 前端: POST base/FileUploadImage (form-data, accept=image/*)
|
||||
* 返回: [{ fileid, host, fileurl, name, type, size }]
|
||||
*/
|
||||
@PostMapping("FileUploadImage")
|
||||
public List<Map<String, Object>> fileUploadImage(
|
||||
@RequestParam("file") MultipartFile[] files,
|
||||
@RequestParam(value = "AppType", required = false) String appType) {
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
UpFileVo vo = fileService.upload(file, appType != null ? appType : "notice", null);
|
||||
results.add(upFileToResult(vo));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载(浏览器直接 location.href 跳转)。
|
||||
* 前端: GET base/FileDown?id=xxx
|
||||
*/
|
||||
@GetMapping("FileDown")
|
||||
public void fileDown(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||
fileService.download(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件预览。
|
||||
* 前端: GET base/FilePreview?id=xxx&type=2
|
||||
*/
|
||||
@GetMapping("FilePreview")
|
||||
public void filePreview(@RequestParam("id") Integer id,
|
||||
@RequestParam(value = "type", required = false) Integer type,
|
||||
HttpServletResponse response) {
|
||||
fileService.download(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件。
|
||||
*/
|
||||
@PostMapping("fileDelete/{fileId}")
|
||||
public void delete(@PathVariable Integer fileId) {
|
||||
fileService.delete(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某通知下的附件列表。
|
||||
*/
|
||||
@GetMapping("fileList/{topicId}")
|
||||
public List<UpFileVo> fileList(@PathVariable Integer topicId) {
|
||||
return fileService.listByTopic("notice", topicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量绑定附件到通知。
|
||||
*/
|
||||
@PostMapping("fileBind")
|
||||
public void bindFiles(@RequestParam("fileIds") List<Integer> fileIds,
|
||||
@RequestParam("topicId") Integer topicId) {
|
||||
fileService.bindToTopic(fileIds, topicId);
|
||||
}
|
||||
|
||||
private Map<String, Object> upFileToResult(UpFileVo vo) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("fileid", vo.getId());
|
||||
result.put("name", vo.getFileTrueName());
|
||||
result.put("fileurl", vo.getFileName());
|
||||
result.put("host", "");
|
||||
result.put("type", vo.getFileType());
|
||||
result.put("size", vo.getFileSize());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||
import com.yunhai.notice.enums.NoticeTypeEnum;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.*;
|
||||
import com.yunhai.notice.repository.NoticeReadRepository;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import com.yunhai.notice.utils.PageUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通知公告前端对接接口。
|
||||
* <p>匹配前端 Notice3.0 的 URL 路径和参数格式。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 分页接口返回 { list, total, pageIndex, pageSize } 结构到 data 字段。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/notice/")
|
||||
public class NoticeApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoticeApiController.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeReadRepository noticeReadRepository;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
// ============================ 已收通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已收通知列表(用户端)。
|
||||
* 前端: GET notice/Notice_list/List
|
||||
*/
|
||||
@GetMapping("Notice_list/List")
|
||||
public Map<String, Object> list(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime,
|
||||
@RequestParam(value = "Name", required = false) String name,
|
||||
@RequestParam(value = "Depttype", required = false) String depttype,
|
||||
@RequestParam(value = "OrderByTime", required = false, defaultValue = "1") String orderByTime,
|
||||
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize,
|
||||
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex) {
|
||||
|
||||
NoticeQuery query = buildQuery(name, depttype, starttime, endtime, orderByTime, pageSize, pageIndex);
|
||||
Sort sort = buildSort(orderByTime);
|
||||
NoticeUserListResult result = noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
|
||||
List<Map<String, Object>> list = result.getContent().stream().map(vo -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("serid", vo.getId());
|
||||
item.put("title", vo.getTitle());
|
||||
item.put("DeptName", vo.getDeptName());
|
||||
item.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
item.put("status", vo.isRead() ? 2 : 1);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("list", list);
|
||||
data.put("total", result.getTotal());
|
||||
data.put("unreadCount", result.getUnreadCount());
|
||||
data.put("pageIndex", pageIndex);
|
||||
data.put("pageSize", pageSize);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================ 已发通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已发/全部通知列表(管理端)。
|
||||
* 前端: GET notice/Notice_list/SendList
|
||||
*/
|
||||
@GetMapping("Notice_list/SendList")
|
||||
public Map<String, Object> sendList(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime,
|
||||
@RequestParam(value = "Name", required = false) String name,
|
||||
@RequestParam(value = "Depttype", required = false) String depttype,
|
||||
@RequestParam(value = "OrderByTime", required = false, defaultValue = "1") String orderByTime,
|
||||
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize,
|
||||
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
|
||||
@RequestParam(value = "type", required = false) String type) {
|
||||
|
||||
NoticeQuery query = buildQuery(name, depttype, starttime, endtime, orderByTime, pageSize, pageIndex);
|
||||
if ("user".equals(type)) {
|
||||
query.setPublisherAccount(NoticeContext.getAccount());
|
||||
}
|
||||
Sort sort = buildSort(orderByTime);
|
||||
BasePageResult<NoticeVo> result = noticeService.adminList(query, PageUtils.toPageable(query, sort));
|
||||
|
||||
List<Map<String, Object>> list = result.getContent().stream().map(vo -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("serid", vo.getId());
|
||||
item.put("title", vo.getTitle());
|
||||
item.put("DeptName", vo.getDeptName());
|
||||
item.put("TeaName", vo.getPublisherName());
|
||||
item.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
item.put("readcount", vo.getReadCount());
|
||||
item.put("allowcount", vo.getAllowCount() != null ? vo.getAllowCount() : 0);
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", vo.getId());
|
||||
String upFileStr = files.stream()
|
||||
.map(f -> String.valueOf(f.getId()))
|
||||
.collect(Collectors.joining(","));
|
||||
item.put("UpFile", upFileStr.isEmpty() ? null : upFileStr);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("list", list);
|
||||
data.put("total", result.getTotal());
|
||||
data.put("pageIndex", pageIndex);
|
||||
data.put("pageSize", pageSize);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================ 查看/编辑通知详情 ============================
|
||||
|
||||
/**
|
||||
* 查看通知详情(type=0 编辑模式不标记已读, type=1 普通查看标记已读)。
|
||||
* 前端: GET notice/Notice_list/Modify
|
||||
*/
|
||||
@GetMapping("Notice_list/Modify")
|
||||
public Map<String, Object> modify(
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||
NoticeVo vo;
|
||||
if (type == 0) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
vo = new NoticeVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setTypeDesc(NoticeTypeEnum.of(entity.getType()).getDesc());
|
||||
vo.setStatusDesc(NoticeStatusEnum.of(entity.getStatus()).getDesc());
|
||||
vo.setReadCount(noticeReadRepository.countByNoticeId(entity.getId()));
|
||||
} else {
|
||||
vo = noticeService.detail(serid);
|
||||
}
|
||||
return voToDetailMap(vo, serid);
|
||||
}
|
||||
|
||||
// ============================ 获取通知详情(编辑用) ============================
|
||||
|
||||
/**
|
||||
* 获取通知详情(编辑/转发用,不标记已读)。
|
||||
* 前端: GET notice/Notice_list/GetNoticeDetails
|
||||
*/
|
||||
@GetMapping("Notice_list/GetNoticeDetails")
|
||||
public Map<String, Object> getNoticeDetails(@RequestParam("serid") Integer serid) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
NoticeVo vo = new NoticeVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setTypeDesc(NoticeTypeEnum.of(entity.getType()).getDesc());
|
||||
vo.setStatusDesc(NoticeStatusEnum.of(entity.getStatus()).getDesc());
|
||||
vo.setReadCount(noticeReadRepository.countByNoticeId(entity.getId()));
|
||||
return voToDetailMap(vo, serid);
|
||||
}
|
||||
|
||||
// ============================ 新建通知 ============================
|
||||
|
||||
/**
|
||||
* 新建通知。
|
||||
* 前端: POST notice/Notice_list/Create (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("Notice_list/Create")
|
||||
public void create(
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "ispull", required = false, defaultValue = "0") Integer ispull,
|
||||
@RequestParam("Contents") String contents,
|
||||
@RequestParam("ReceiveList") String receiveList,
|
||||
@RequestParam("DeptID") String deptId,
|
||||
@RequestParam(value = "ChaoSend", required = false) String chaoSend,
|
||||
@RequestParam(value = "topmost", required = false, defaultValue = "0") Integer topmost,
|
||||
@RequestParam(value = "DsfsTime", required = false) String dsfsTime) {
|
||||
|
||||
NoticeRequest request = buildNoticeRequest(title, contents, receiveList, deptId, chaoSend, topmost, dsfsTime);
|
||||
noticeService.publish(request);
|
||||
|
||||
if (StringUtils.hasText(chaoSend)) {
|
||||
List<Integer> fileIds = parseIds(chaoSend);
|
||||
Page<Notice> latest = noticeRepository.findAll(
|
||||
PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createTime")));
|
||||
if (latest.hasContent()) {
|
||||
fileService.bindToTopic(fileIds, latest.getContent().get(0).getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 编辑通知 ============================
|
||||
|
||||
/**
|
||||
* 编辑通知保存。
|
||||
* 前端: POST notice/Notice_list/EditNotice (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("Notice_list/EditNotice")
|
||||
public void editNotice(
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "ispull", required = false, defaultValue = "0") Integer ispull,
|
||||
@RequestParam("Contents") String contents,
|
||||
@RequestParam("ReceiveList") String receiveList,
|
||||
@RequestParam("DeptID") String deptId,
|
||||
@RequestParam(value = "ChaoSend", required = false) String chaoSend,
|
||||
@RequestParam(value = "topmost", required = false, defaultValue = "0") Integer topmost,
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "DsfsTime", required = false) String dsfsTime) {
|
||||
|
||||
NoticeRequest request = buildNoticeRequest(title, contents, receiveList, deptId, chaoSend, topmost, dsfsTime);
|
||||
noticeService.edit(serid, request);
|
||||
|
||||
if (StringUtils.hasText(chaoSend)) {
|
||||
List<Integer> fileIds = parseIds(chaoSend);
|
||||
fileService.bindToTopic(fileIds, serid);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 删除通知 ============================
|
||||
|
||||
/**
|
||||
* 删除通知(支持批量)。
|
||||
* 前端: POST notice/Notice_list/DeleteNotice (form-urlencoded: ids=1,2,3)
|
||||
*/
|
||||
@PostMapping("Notice_list/DeleteNotice")
|
||||
public void deleteNotice(@RequestParam("ids") String ids) {
|
||||
for (String idStr : ids.split(",")) {
|
||||
String trimmed = idStr.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
noticeService.delete(Integer.parseInt(trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 阅读情况 ============================
|
||||
|
||||
/**
|
||||
* 按组分类查看阅读情况。
|
||||
* 前端: GET notice/Notice_list/OrgizeModify
|
||||
*/
|
||||
@GetMapping("Notice_list/OrgizeModify")
|
||||
public List<Map<String, Object>> orgizeModify(@RequestParam("serid") Integer serid) {
|
||||
List<NoticeVo.ReadDetail> readDetails = noticeService.getReadDetail(serid);
|
||||
List<Map<String, Object>> teaslist = readDetails.stream().map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("teaname", d.getName());
|
||||
item.put("states", "read");
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> group = new LinkedHashMap<>();
|
||||
group.put("GroupName", "已读人员");
|
||||
group.put("teaslist", teaslist);
|
||||
return Collections.singletonList(group);
|
||||
}
|
||||
|
||||
// ============================ 追加接收人 ============================
|
||||
|
||||
/**
|
||||
* 追加接收人。
|
||||
* 前端: POST notice/Notice_list/ReceiveUsers
|
||||
*/
|
||||
@PostMapping("Notice_list/ReceiveUsers")
|
||||
public void receiveUsers(
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam("users") Integer[] users) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
int addCount = users != null ? users.length : 0;
|
||||
entity.setAllowCount((entity.getAllowCount() == null ? 0 : entity.getAllowCount()) + addCount);
|
||||
noticeRepository.save(entity);
|
||||
}
|
||||
|
||||
// ============================ 一键全部已读 ============================
|
||||
|
||||
/**
|
||||
* 一键全部已读。
|
||||
* 前端: GET notice/Notice_list/InitializeRed
|
||||
*/
|
||||
@GetMapping("Notice_list/InitializeRed")
|
||||
public void initializeRed() {
|
||||
String account = NoticeContext.getAccount();
|
||||
List<Integer> readIds = noticeReadRepository.findNoticeIdsByReaderAccount(account);
|
||||
Set<Integer> readSet = new HashSet<>(readIds);
|
||||
List<Notice> published = noticeRepository.findAll((root, query, cb) ->
|
||||
cb.equal(root.get("status"), NoticeStatusEnum.PUBLISHED.getCode()));
|
||||
for (Notice n : published) {
|
||||
if (!readSet.contains(n.getId())) {
|
||||
noticeService.markRead(n.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 批量下载附件 ============================
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件。
|
||||
* 前端: GET notice/Notice_list/AllFileDown?dataid=xxx
|
||||
*/
|
||||
@GetMapping("Notice_list/AllFileDown")
|
||||
public void allFileDown(@RequestParam("dataid") Integer dataid,
|
||||
javax.servlet.http.HttpServletResponse response) {
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", dataid);
|
||||
if (files.isEmpty()) {
|
||||
throw new NoticeException("无附件可下载");
|
||||
}
|
||||
if (files.size() == 1) {
|
||||
fileService.download(files.get(0).getId(), response);
|
||||
} else {
|
||||
fileService.download(files.get(0).getId(), response);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 通知统计 ============================
|
||||
|
||||
/**
|
||||
* 通知统计。
|
||||
* 前端: GET notice/Notice_statis/List
|
||||
*/
|
||||
@GetMapping("Notice_statis/List")
|
||||
public NoticeStatisticsVo statisData(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime) {
|
||||
LocalDateTime start = StringUtils.hasText(starttime)
|
||||
? LocalDateTime.parse(starttime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
|
||||
LocalDateTime end = StringUtils.hasText(endtime)
|
||||
? LocalDateTime.parse(endtime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
|
||||
return noticeService.statistics(new NoticeQuery(), start, end);
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
private NoticeQuery buildQuery(String name, String depttype, String starttime, String endtime,
|
||||
String orderByTime, int pageSize, int pageIndex) {
|
||||
NoticeQuery query = new NoticeQuery();
|
||||
query.setKeyword(name);
|
||||
query.setDeptId(depttype);
|
||||
query.setPageSize(pageSize);
|
||||
query.setPageIndex(pageIndex);
|
||||
query.setOrderByTime(orderByTime);
|
||||
if (StringUtils.hasText(starttime)) {
|
||||
query.setStartTime(LocalDate.parse(starttime).atStartOfDay());
|
||||
}
|
||||
if (StringUtils.hasText(endtime)) {
|
||||
query.setEndTime(LocalDate.parse(endtime).atTime(23, 59, 59));
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private Sort buildSort(String orderByTime) {
|
||||
Sort sort = "2".equals(orderByTime)
|
||||
? Sort.by(Sort.Direction.ASC, "createTime")
|
||||
: Sort.by(Sort.Direction.DESC, "createTime");
|
||||
return sort.and(Sort.by(Sort.Direction.DESC, "topmost"));
|
||||
}
|
||||
|
||||
private NoticeRequest buildNoticeRequest(String title, String contents, String receiveList,
|
||||
String deptId, String chaoSend, Integer topmost, String dsfsTime) {
|
||||
NoticeRequest request = new NoticeRequest();
|
||||
request.setTitle(title);
|
||||
request.setContent(contents);
|
||||
request.setReceiveList(receiveList);
|
||||
request.setDeptId(deptId);
|
||||
request.setChaoSend(chaoSend);
|
||||
request.setTopmost(topmost);
|
||||
request.setSmsFlag(topmost);
|
||||
if (StringUtils.hasText(dsfsTime)) {
|
||||
request.setTimingSendTime(LocalDateTime.parse(dsfsTime.replace("/", "-"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private Map<String, Object> voToDetailMap(NoticeVo vo, Integer serid) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("title", vo.getTitle());
|
||||
data.put("Contents", vo.getContent());
|
||||
data.put("DeptName", vo.getDeptName());
|
||||
data.put("DeptID", vo.getDeptId());
|
||||
data.put("TeaName", vo.getPublisherName());
|
||||
data.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
data.put("numcount", vo.getAllowCount() != null ? vo.getAllowCount() : 0);
|
||||
data.put("readnumcount", vo.getReadCount());
|
||||
data.put("ReceiveList", vo.getReceiveList());
|
||||
data.put("DsfsTime", vo.getTimingSendTime() != null ? formatTime(vo.getTimingSendTime()) : null);
|
||||
data.put("isDsfs", vo.getTimingSendTime() != null);
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", serid);
|
||||
List<Map<String, Object>> chaoSend = files.stream().map(f -> {
|
||||
Map<String, Object> fileMap = new LinkedHashMap<>();
|
||||
fileMap.put("id", f.getId());
|
||||
fileMap.put("name", f.getFileTrueName());
|
||||
fileMap.put("size", f.getFileSize());
|
||||
fileMap.put("fileid", f.getId());
|
||||
fileMap.put("filename", f.getFileName());
|
||||
return fileMap;
|
||||
}).collect(Collectors.toList());
|
||||
data.put("ChaoSend", chaoSend);
|
||||
data.put("tlist", "all");
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<Integer> parseIds(String idsStr) {
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
for (String s : idsStr.split(",")) {
|
||||
String trimmed = s.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
try {
|
||||
ids.add(Integer.parseInt(trimmed));
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[parseIds] 无法解析文件ID: {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private String formatTime(LocalDateTime time) {
|
||||
if (time == null) return null;
|
||||
return time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.NoticeQuery;
|
||||
import com.yunhai.notice.model.notice.NoticeRequest;
|
||||
import com.yunhai.notice.model.notice.NoticeStatisticsVo;
|
||||
import com.yunhai.notice.model.notice.NoticeUserListResult;
|
||||
import com.yunhai.notice.model.notice.NoticeVo;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import com.yunhai.notice.utils.PageUtils;
|
||||
import com.yunhai.platform.apistd.ApiResponse;
|
||||
import com.yunhai.platform.openapi.client.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.openapi.model.SysUserRequest;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知公告接口。
|
||||
* <p>路径前缀:/{platform}/base/(platform = api / manage / mobiapi),
|
||||
* 与 UserBase BaseController 路径习惯一致,前端无需区分子应用。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||
public class NoticeController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoticeController.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Autowired
|
||||
private OpenApiUserClient openApiUserClient;
|
||||
|
||||
@GetMapping("GetUserInfo")
|
||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||
return authUserWithDept;
|
||||
}
|
||||
|
||||
@GetMapping("GetUserMod")
|
||||
public List<BoaModule> getUserMod(BoaUser authUserWithDept) {
|
||||
Integer schoolId = (authUserWithDept.getSchool() != null) ? authUserWithDept.getSchool().getId() : null;
|
||||
List<BoaModule> modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schoolId);
|
||||
return modules;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("GetTree")
|
||||
public void GetTree(BoaUser authUserWithDept) {
|
||||
YschBaseOpenApi.deleteUser(10022,6,27);
|
||||
}
|
||||
|
||||
/** 发通知 */
|
||||
@PostMapping("publish")
|
||||
public void publish(@RequestBody NoticeRequest request) {
|
||||
noticeService.publish(request);
|
||||
}
|
||||
|
||||
/** 保存草稿 */
|
||||
@PostMapping("saveDraft")
|
||||
public Integer saveDraft(@RequestBody NoticeRequest request) {
|
||||
return noticeService.saveDraft(request);
|
||||
}
|
||||
|
||||
/** 编辑通知 */
|
||||
@PostMapping("edit/{id}")
|
||||
public void edit(@PathVariable Integer id, @RequestBody NoticeRequest request) {
|
||||
noticeService.edit(id, request);
|
||||
}
|
||||
|
||||
/** 删除通知 */
|
||||
@PostMapping("delete/{id}")
|
||||
public void delete(@PathVariable Integer id) {
|
||||
noticeService.delete(id);
|
||||
}
|
||||
|
||||
/** 撤回通知 */
|
||||
@PostMapping("pull/{id}")
|
||||
public void pull(@PathVariable Integer id) {
|
||||
noticeService.pull(id);
|
||||
}
|
||||
|
||||
/** 置顶/取消置顶 */
|
||||
@PostMapping("setTopmost/{id}/{topmost}")
|
||||
public void setTopmost(@PathVariable Integer id, @PathVariable Integer topmost) {
|
||||
noticeService.setTopmost(id, topmost);
|
||||
}
|
||||
|
||||
/** 转发通知(基于源通知创建草稿) */
|
||||
@PostMapping("forward/{sourceId}")
|
||||
public Integer forward(@PathVariable Integer sourceId, @RequestBody NoticeRequest request) {
|
||||
return noticeService.forward(sourceId, request);
|
||||
}
|
||||
|
||||
/** 阅读明细列表 */
|
||||
@GetMapping("readDetail/{noticeId}")
|
||||
public List<NoticeVo.ReadDetail> readDetail(@PathVariable Integer noticeId) {
|
||||
return noticeService.getReadDetail(noticeId);
|
||||
}
|
||||
|
||||
/** 用户端列表:仅已发布通知,标记当前用户是否已读,附未读总数(列表有新发的通知) */
|
||||
@GetMapping("userList")
|
||||
public NoticeUserListResult userList(NoticeQuery query,
|
||||
@SortDefault(sort = {"topmost", "createTime"},
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
/** 管理端列表:管理员查看全部通知 */
|
||||
@GetMapping("adminList")
|
||||
public BasePageResult<NoticeVo> adminList(NoticeQuery query,
|
||||
@SortDefault(sort = "createTime",
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.adminList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
/** 通知详情:返回详情并顺带标记当前用户已读 */
|
||||
@GetMapping("detail/{id}")
|
||||
public NoticeVo detail(@PathVariable Integer id) {
|
||||
return noticeService.detail(id);
|
||||
}
|
||||
|
||||
/** 标记已读(前端主动调用,幂等) */
|
||||
@PostMapping("read/{id}")
|
||||
public void read(@PathVariable Integer id) {
|
||||
noticeService.markRead(id);
|
||||
}
|
||||
|
||||
/** 通知统计:总数/已发布/草稿、累计阅读人次、当前用户未读、按发布人、按月 */
|
||||
@GetMapping("statistics")
|
||||
public NoticeStatisticsVo statistics(@RequestParam(required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
|
||||
@RequestParam(required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) {
|
||||
return noticeService.statistics(new NoticeQuery(), start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.SmsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 短信设置接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/schsms/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/schsms/")
|
||||
public class SmsController {
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
// ============================ getmodule ============================
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
* 前端: GET schsms/getmodule
|
||||
*/
|
||||
@GetMapping("getmodule")
|
||||
public List<Map<String, Object>> getModuleList() {
|
||||
return smsService.getModuleList();
|
||||
}
|
||||
|
||||
// ============================ getgroup ============================
|
||||
|
||||
/**
|
||||
* 获取学校父类组列表。
|
||||
* 前端: GET schsms/getgroup
|
||||
*/
|
||||
@GetMapping("getgroup")
|
||||
public List<Map<String, Object>> getSchParentGroup() {
|
||||
return smsService.getSchParentGroup();
|
||||
}
|
||||
|
||||
// ============================ getschsms ============================
|
||||
|
||||
/**
|
||||
* 分页获取学校短信开关列表。
|
||||
* 前端: GET schsms/getschsms?PageSize=&PageIndex=&SchName=
|
||||
*/
|
||||
@GetMapping("getschsms")
|
||||
public Map<String, Object> getSchIsSmsList(
|
||||
@RequestParam(value = "schpid", required = false) String schpid,
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "SchName", required = false, defaultValue = "") String schName) {
|
||||
// 前端 PageIndex 从0开始(page-1),后端按0基处理
|
||||
return smsService.getSchIsSmsList(pageSize, pageIndex, schName);
|
||||
}
|
||||
|
||||
// ============================ getsch ============================
|
||||
|
||||
/**
|
||||
* 获取学校列表。
|
||||
* 前端: GET schsms/getsch
|
||||
*/
|
||||
@GetMapping("getsch")
|
||||
public List<Map<String, Object>> getSchList() {
|
||||
return smsService.getSchList();
|
||||
}
|
||||
|
||||
// ============================ setschsms ============================
|
||||
|
||||
/**
|
||||
* 设置单校单模块短信开关。
|
||||
* 前端: POST schsms/setschsms (form-urlencoded: itemsModu=JSON)
|
||||
* C# 从 form 字段 itemsModu 读取 JSON: {SerID, ModuID, IsSms, SchSerID}
|
||||
*/
|
||||
@PostMapping("setschsms")
|
||||
public void setSchSmsTrue(@RequestParam("itemsModu") String itemsModuJson) {
|
||||
// 解析 JSON: {"SerID":1,"ModuID":2,"IsSms":true,"SchSerID":100}
|
||||
// 简化处理:直接提取字段值
|
||||
// 前端传的是 JSON 字符串,需解析
|
||||
Integer schSerID = extractJsonField(itemsModuJson, "SchSerID");
|
||||
Integer moduID = extractJsonField(itemsModuJson, "ModuID");
|
||||
Boolean isSms = extractJsonBool(itemsModuJson, "IsSms");
|
||||
smsService.setSchSms(schSerID, moduID, isSms);
|
||||
}
|
||||
|
||||
// ============================ setschallsms ============================
|
||||
|
||||
/**
|
||||
* 设置某校全部模块短信开关。
|
||||
* 前端: GET schsms/setschallsms?SchSerID=&IsSms=
|
||||
*/
|
||||
@GetMapping("setschallsms")
|
||||
public void setSchSmsAllTrue(
|
||||
@RequestParam("SchSerID") Integer schSerID,
|
||||
@RequestParam("IsSms") Boolean isSms) {
|
||||
smsService.setSchSmsAll(schSerID, isSms);
|
||||
}
|
||||
|
||||
// ============================ setschmodulesms ============================
|
||||
|
||||
/**
|
||||
* 批量设置多校多模块短信开关。
|
||||
* 前端: GET schsms/setschmodulesms?SchSerID=1,2&Module=3,4&IsSms=true
|
||||
*/
|
||||
@GetMapping("setschmodulesms")
|
||||
public void setSchModuleSmsTrue(
|
||||
@RequestParam("SchSerID") String schSerID,
|
||||
@RequestParam("Module") String module,
|
||||
@RequestParam("IsSms") Boolean isSms) {
|
||||
smsService.setSchModuleSms(schSerID, module, isSms);
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取整数字段(简化版,不引入完整JSON库)。
|
||||
*/
|
||||
private Integer extractJsonField(String json, String field) {
|
||||
if (json == null) return null;
|
||||
// 匹配 "field": value 或 "field": "value"
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*\"?(-?\\d+)\"?";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Integer.parseInt(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取布尔字段。
|
||||
*/
|
||||
private Boolean extractJsonBool(String json, String field) {
|
||||
if (json == null) return null;
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*(true|false)";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Boolean.parseBoolean(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.TemplateService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 通知模板接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/notice/Template/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/notice/Template/")
|
||||
public class TemplateController {
|
||||
|
||||
@Autowired
|
||||
private TemplateService templateService;
|
||||
|
||||
// ============================ GetTypelist ============================
|
||||
|
||||
/**
|
||||
* 获取模板类型列表。
|
||||
* 前端: GET notice/Template/GetTypelist
|
||||
*/
|
||||
@GetMapping("GetTypelist")
|
||||
public List<Map<String, Object>> getTypelist() {
|
||||
return templateService.getTypelist();
|
||||
}
|
||||
|
||||
// ============================ EditType ============================
|
||||
|
||||
/**
|
||||
* 新增/编辑模板类型。
|
||||
* 前端: GET notice/Template/EditType?typeid=&TypeName=
|
||||
*/
|
||||
@GetMapping("EditType")
|
||||
public void editType(
|
||||
@RequestParam("typeid") Integer typeid,
|
||||
@RequestParam("TypeName") String typeName) {
|
||||
templateService.editType(typeid, typeName);
|
||||
}
|
||||
|
||||
// ============================ DelType ============================
|
||||
|
||||
/**
|
||||
* 删除模板类型。
|
||||
* 前端: GET notice/Template/DelType?typeid=
|
||||
*/
|
||||
@GetMapping("DelType")
|
||||
public void delType(@RequestParam("typeid") Integer typeid) {
|
||||
templateService.delType(typeid);
|
||||
}
|
||||
|
||||
// ============================ GetTemplatelist ============================
|
||||
|
||||
/**
|
||||
* 分页获取模板列表。
|
||||
* 前端: GET notice/Template/GetTemplatelist?PageSize=&PageIndex=&typeid=
|
||||
*/
|
||||
@GetMapping("GetTemplatelist")
|
||||
public Map<String, Object> getTemplatelist(
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "typeid", defaultValue = "0") Integer typeid) {
|
||||
return templateService.getTemplatelist(pageSize, pageIndex, typeid);
|
||||
}
|
||||
|
||||
// ============================ EditTemplate ============================
|
||||
|
||||
/**
|
||||
* 新增/编辑模板。
|
||||
* 前端: POST notice/Template/EditTemplate (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("EditTemplate")
|
||||
public void editTemplate(
|
||||
@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);
|
||||
}
|
||||
|
||||
// ============================ DelTemplate ============================
|
||||
|
||||
/**
|
||||
* 删除模板。
|
||||
* 前端: GET notice/Template/DelTemplate?serid=
|
||||
*/
|
||||
@GetMapping("DelTemplate")
|
||||
public void delTemplate(@RequestParam("SerID") Integer serid) {
|
||||
templateService.delTemplate(serid);
|
||||
}
|
||||
|
||||
// ============================ GetTemplateFist ============================
|
||||
|
||||
/**
|
||||
* 获取单个模板详情。
|
||||
* 前端: GET notice/Template/GetTemplateFist?serid=
|
||||
*/
|
||||
@GetMapping("GetTemplateFist")
|
||||
public Map<String, Object> getTemplateFist(@RequestParam("serid") Integer serid) {
|
||||
return templateService.getTemplateFist(serid);
|
||||
}
|
||||
}
|
||||
@@ -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,147 @@
|
||||
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.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 通知标题
|
||||
*/
|
||||
@NotBlank(message = "通知标题不得为空")
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 通知内容
|
||||
*/
|
||||
@Column(name = "content", columnDefinition = "LONGTEXT")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.notice.enums.NoticeTypeEnum})
|
||||
*/
|
||||
@NotNull
|
||||
@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;
|
||||
|
||||
/**
|
||||
* 状态:0 草稿 / 1 已发布(见 {@link com.yunhai.notice.enums.NoticeStatusEnum})
|
||||
*/
|
||||
@NotNull
|
||||
@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;
|
||||
}
|
||||
@@ -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.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,33 @@
|
||||
package com.yunhai.notice.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通知状态。对应原 .NET Notify_Message.status(原系统已发布=2,此处归一化)。
|
||||
*/
|
||||
@Getter
|
||||
public enum NoticeStatusEnum {
|
||||
|
||||
DRAFT(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,58 @@
|
||||
package com.yunhai.notice.job;
|
||||
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时发送任务:每分钟扫描草稿状态且 timingSendTime <= now 的通知,自动发布。
|
||||
* <p>
|
||||
* 多实例部署时需加分布式锁(ShedLock 或 Quartz 集群),避免重复执行。
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "timing-send.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class TimingSendJob {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TimingSendJob.class);
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Value("${timing-send.cron:0 * * * * ?}")
|
||||
private String cron;
|
||||
|
||||
/**
|
||||
* 每分钟执行一次,扫描到期的定时通知并发送。
|
||||
*/
|
||||
@Scheduled(cron = "${timing-send.cron:0 * * * * ?}")
|
||||
public void executeTimingSend() {
|
||||
List<Notice> pendingList = noticeRepository.findPendingTimingSend(
|
||||
NoticeStatusEnum.DRAFT.getCode(), LocalDateTime.now());
|
||||
if (pendingList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("[TimingSendJob] 发现 {} 条待发送通知", pendingList.size());
|
||||
for (Notice notice : pendingList) {
|
||||
try {
|
||||
noticeService.sendNotify(notice.getId());
|
||||
log.info("[TimingSendJob] 通知 {} 定时发送成功", notice.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[TimingSendJob] 通知 {} 定时发送失败", notice.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,45 @@
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知列表查询条件。
|
||||
* <p>
|
||||
* - 管理端(adminList):用 status / type / deptId / publisherAccount / 时间范围过滤;
|
||||
* - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class NoticeQuery extends BasePageQuery {
|
||||
|
||||
/** 状态过滤:null 不过滤;0 草稿 / 1 已发布 */
|
||||
private Integer status;
|
||||
|
||||
/** 类型过滤:1 普通 / 2 重要 */
|
||||
private Integer type;
|
||||
|
||||
/** 发布部门ID */
|
||||
private String deptId;
|
||||
|
||||
/** 发布人账号(管理端按发布人查) */
|
||||
private String publisherAccount;
|
||||
|
||||
/** 开始时间(创建时间 >=) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/** 结束时间(创建时间 <=) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||
private Integer readType = 0;
|
||||
|
||||
/** 排序:1 时间倒序(默认)/ 2 时间正序 */
|
||||
private String orderByTime = "1";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeRequest {
|
||||
|
||||
/** 通知标题 */
|
||||
@NotBlank(message = "通知标题不得为空")
|
||||
private String title;
|
||||
|
||||
/** 通知内容 */
|
||||
private String content;
|
||||
|
||||
/** 通知类型:1 普通 / 2 重要 */
|
||||
@NotNull(message = "通知类型不得为空")
|
||||
private Integer type = 1;
|
||||
|
||||
/** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */
|
||||
@NotBlank(message = "发布部门不得为空")
|
||||
private String deptId;
|
||||
|
||||
/** 发布部门名称 */
|
||||
private String deptName;
|
||||
|
||||
/** 是否置顶:0 否 / 1 是 */
|
||||
private Integer topmost = 0;
|
||||
|
||||
/** 接收人列表(JSON 数组字符串,如 ["account1","account2"]) */
|
||||
private String receiveList;
|
||||
|
||||
/** 抄送人列表 */
|
||||
private String chaoSend;
|
||||
|
||||
/** 定时发送时间(为空表示立即发布) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 短信发送标记:0 不发短信 / 1 发短信 */
|
||||
private Integer smsFlag = 0;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 draftCount;
|
||||
|
||||
/** 阅读记录总数(累计已读人次) */
|
||||
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,65 @@
|
||||
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;
|
||||
|
||||
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,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,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,29 @@
|
||||
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 java.util.List;
|
||||
|
||||
public interface ContactsGroupUserRepository extends JpaRepository<ContactsGroupUser, Integer> {
|
||||
Page<ContactsGroupUser> findByGroupId(Integer groupId, Pageable pageable);
|
||||
List<ContactsGroupUser> findByGroupId(Integer groupId);
|
||||
void deleteByGroupId(Integer groupId);
|
||||
|
||||
/**
|
||||
* 按多个组ID查询所有成员(用于统计每组人数、组树构建)。
|
||||
*/
|
||||
List<ContactsGroupUser> findByGroupIdIn(List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按多个组ID批量删除成员。
|
||||
*/
|
||||
void deleteByGroupIdIn(List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按组ID和用户ID查询(判断是否已存在)。
|
||||
*/
|
||||
boolean existsByGroupIdAndUserId(Integer groupId, Integer userId);
|
||||
}
|
||||
@@ -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,48 @@
|
||||
package com.yunhai.notice.repository;
|
||||
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
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);
|
||||
|
||||
/**
|
||||
* 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。
|
||||
*/
|
||||
@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);
|
||||
|
||||
/**
|
||||
* 查询待定时发送的通知:状态为草稿、timingSendTime 不为空且 <= 当前时间。
|
||||
*/
|
||||
@Query("select n from Notice n where n.status = :status " +
|
||||
"and n.timingSendTime is not null and n.timingSendTime <= :now")
|
||||
List<Notice> findPendingTimingSend(@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
}
|
||||
@@ -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,11 @@
|
||||
package com.yunhai.notice.repository;
|
||||
|
||||
import com.yunhai.notice.entity.SysDict;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SysDictRepository extends JpaRepository<SysDict, Integer> {
|
||||
List<SysDict> findByDictTypeOrderBySortOrderAsc(String dictType);
|
||||
List<SysDict> findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag);
|
||||
}
|
||||
@@ -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,22 @@
|
||||
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删除附件(通知删除时级联清理)。
|
||||
*/
|
||||
void deleteByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 ContactsService {
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
*
|
||||
* @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();
|
||||
|
||||
/**
|
||||
* 导入联系组(Excel)。
|
||||
* C# 读取Excel文件,逐行匹配组名+教师。Java版预留。
|
||||
*
|
||||
* @return 导入结果 Map
|
||||
*/
|
||||
Map<String, Object> importContacts();
|
||||
|
||||
/**
|
||||
* 导出联系组(Excel)。
|
||||
* C# 生成Excel文件流返回。Java版预留。
|
||||
*
|
||||
* @return 导出数据 Map
|
||||
*/
|
||||
Map<String, Object> exportContacts();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典业务接口。
|
||||
* 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。
|
||||
*/
|
||||
public interface DictService {
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 发文单位同步(从用户中心组织架构同步部门到字典)。
|
||||
*/
|
||||
void getDeptSync();
|
||||
|
||||
/**
|
||||
* 是否发送短信(查询当前学校模块短信开关)。
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean getSchModuleIsSms();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务接口。
|
||||
*/
|
||||
public interface FileService {
|
||||
|
||||
/**
|
||||
* 上传单个文件。
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* 删除文件(同时删除物理文件和数据库记录)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
*/
|
||||
void delete(Integer fileId);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.NoticeQuery;
|
||||
import com.yunhai.notice.model.notice.NoticeRequest;
|
||||
import com.yunhai.notice.model.notice.NoticeStatisticsVo;
|
||||
import com.yunhai.notice.model.notice.NoticeUserListResult;
|
||||
import com.yunhai.notice.model.notice.NoticeVo;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知公告业务接口。
|
||||
*/
|
||||
public interface INoticeService {
|
||||
|
||||
/**
|
||||
* 发通知:以当前登录用户为发布人,状态置为「已发布」入库。
|
||||
* 若 request.timingSendTime 不为空且晚于当前时间,则置为草稿等待定时发送。
|
||||
*/
|
||||
void publish(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 保存草稿:状态置为「草稿」,不发布。
|
||||
*/
|
||||
Integer saveDraft(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 编辑通知:按 id 更新标题/内容/类型/部门/置顶,发布人与状态保持不变。
|
||||
*/
|
||||
void edit(Integer id, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 删除通知:删除主记录并级联清理其阅读记录。
|
||||
*/
|
||||
void delete(Integer id);
|
||||
|
||||
/**
|
||||
* 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。
|
||||
*/
|
||||
void pull(Integer id);
|
||||
|
||||
/**
|
||||
* 置顶/取消置顶。
|
||||
*/
|
||||
void setTopmost(Integer id, Integer topmost);
|
||||
|
||||
/**
|
||||
* 转发通知:基于源通知创建新草稿,parentId 关联源通知。
|
||||
*/
|
||||
Integer forward(Integer sourceId, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 发送通知(供定时任务和手动发送调用):更新状态为已发布,异步推送消息。
|
||||
*/
|
||||
void sendNotify(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 用户端列表:仅展示已发布通知,并标记当前用户是否已读,附未读总数。
|
||||
*/
|
||||
NoticeUserListResult userList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 管理端列表:按条件查询全部通知(管理员查看全部)。
|
||||
*/
|
||||
BasePageResult<NoticeVo> adminList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。
|
||||
*/
|
||||
NoticeVo detail(Integer id);
|
||||
|
||||
/**
|
||||
* 标记已读:为当前用户写入(幂等)该通知的阅读记录。
|
||||
*/
|
||||
void markRead(Integer id);
|
||||
|
||||
/**
|
||||
* 获取某通知的阅读明细列表。
|
||||
*/
|
||||
List<NoticeVo.ReadDetail> getReadDetail(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 通知统计:总数/已发布/草稿、累计阅读人次、当前用户未读、按发布人、按月。
|
||||
*/
|
||||
NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信设置业务接口。
|
||||
* 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。
|
||||
*/
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
* 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);
|
||||
}
|
||||
@@ -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 TemplateService {
|
||||
|
||||
/**
|
||||
* 获取模板类型列表(首项固定为"全部")。
|
||||
*
|
||||
* @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,406 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
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.ContactsService;
|
||||
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 ContactsServiceImpl implements ContactsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ContactsServiceImpl.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 NoticeException("请输入自定义组的名字");
|
||||
}
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
|
||||
// 查重:同创建人下同名同类型组不重复
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该联系组!");
|
||||
}
|
||||
|
||||
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 = NoticeContext.getAccount();
|
||||
|
||||
// 查询当前用户创建的全部联系组
|
||||
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 NoticeException("联系组名称不能为空");
|
||||
}
|
||||
if (id == null) {
|
||||
throw new NoticeException("参数错误!");
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
|
||||
// 查重(排除自身)
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
!g.getId().equals(id) && name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该联系组");
|
||||
}
|
||||
|
||||
ContactsGroup group = groupRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("联系组不存在"));
|
||||
group.setGroupName(name);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
|
||||
// ============================ DelGroup ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delGroup(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new NoticeException("参数错误,删除失败");
|
||||
}
|
||||
List<Integer> idList = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 先删成员再删组
|
||||
groupUserRepository.deleteByGroupIdIn(idList);
|
||||
groupRepository.findAllById(idList).forEach(groupRepository::delete);
|
||||
}
|
||||
|
||||
// ============================ SetContactsTea ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setContactsTea(Integer gid, String ids) {
|
||||
if (gid == null || gid == 0) {
|
||||
throw new NoticeException("参数错误");
|
||||
}
|
||||
// 全量替换:先删除该组全部成员,再新增
|
||||
groupUserRepository.deleteByGroupId(gid);
|
||||
|
||||
if (StringUtils.hasText(ids)) {
|
||||
List<Integer> userIds = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.collect(Collectors.toList());
|
||||
for (Integer userId : userIds) {
|
||||
ContactsGroupUser member = new ContactsGroupUser();
|
||||
member.setGroupId(gid);
|
||||
member.setUserId(userId);
|
||||
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<Map<String, Object>> listData = allMembers.stream().map(m -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("sid", m.getId());
|
||||
item.put("id", m.getUserId());
|
||||
item.put("name", m.getUserName() != null ? m.getUserName() : "");
|
||||
item.put("account", m.getUserAccount() != null ? m.getUserAccount() : "");
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 分页
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getGroupTree(Integer type) {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
|
||||
// 查询当前用户的联系组
|
||||
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("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", m.getUserName() != null ? m.getUserName() : "");
|
||||
node.put("pid", m.getGroupId());
|
||||
node.put("no", 0);
|
||||
node.put("type", 2);
|
||||
node.put("photo", "");
|
||||
treeNodes.add(node);
|
||||
}
|
||||
|
||||
// 递归构建树
|
||||
List<Map<String, Object>> tree = formatUserTree(treeNodes, 0);
|
||||
|
||||
// 包裹根节点
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("id", 0);
|
||||
root.put("name", "通知公告系统");
|
||||
root.put("children", tree);
|
||||
root.put("no", "0");
|
||||
root.put("type", 1);
|
||||
|
||||
return Collections.singletonList(root);
|
||||
}
|
||||
|
||||
// ============================ GetDeptRoleGroupTree ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||
// C# 从外部API获取部门树+用户角色映射。
|
||||
// Java版暂未接入用户中心OpenAPI,返回空列表。
|
||||
log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ Import ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> importContacts() {
|
||||
// C# 读取上传的Excel文件逐行匹配组名+教师。
|
||||
// Java版暂未实现Excel解析,预留接口。
|
||||
log.info("[importContacts] Excel导入功能暂未实现");
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("msg", "导入功能暂未实现");
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ Export ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> exportContacts() {
|
||||
// C# 生成Excel文件流返回。
|
||||
// Java版暂未实现Excel导出,预留接口。
|
||||
log.info("[exportContacts] Excel导出功能暂未实现");
|
||||
|
||||
// 返回联系组数据供前端参考
|
||||
String account = NoticeContext.getAccount();
|
||||
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>> rows = new ArrayList<>();
|
||||
for (ContactsGroup g : groups) {
|
||||
List<ContactsGroupUser> members = allMembers.stream()
|
||||
.filter(m -> m.getGroupId().equals(g.getId()))
|
||||
.collect(Collectors.toList());
|
||||
if (members.isEmpty()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("组名", g.getGroupName());
|
||||
row.put("教师", "");
|
||||
row.put("用户名", "");
|
||||
rows.add(row);
|
||||
} else {
|
||||
for (ContactsGroupUser m : members) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("组名", g.getGroupName());
|
||||
row.put("教师", m.getUserName() != null ? m.getUserName() : "");
|
||||
row.put("用户名", m.getUserAccount() != null ? m.getUserAccount() : "");
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", rows);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 递归构建用户树(对应 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
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.DictService;
|
||||
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 DictServiceImpl implements DictService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DictServiceImpl.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 NoticeException("参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1);
|
||||
if (list.isEmpty()) {
|
||||
throw new NoticeException("尚未添加发文部门,请联系管理员添加!");
|
||||
}
|
||||
// 按 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 NoticeException("非超管不得操作!");
|
||||
}
|
||||
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 NoticeException("输入字符串格式不正确!");
|
||||
}
|
||||
// 校验重名
|
||||
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 NoticeException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
// 计算 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 NoticeException("参数错误!");
|
||||
}
|
||||
// 校验重名(排除自身)
|
||||
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 NoticeException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("字典记录不存在"));
|
||||
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 NoticeException("字典记录不存在"));
|
||||
// 校验是否被通知引用(按 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 NoticeException("已引用该部门,无法删除!");
|
||||
}
|
||||
sysDictRepository.delete(dict);
|
||||
}
|
||||
|
||||
// ============================ OrdbyDicIds ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void ordbyDicIds(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new NoticeException("参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
if (list.isEmpty()) {
|
||||
throw new NoticeException("字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
// 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]--;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ GetDeptSync ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void getDeptSync() {
|
||||
// C# 从用户中心 OpenAPI 获取学校部门树并同步到字典。
|
||||
// Java 版暂不接入外部 OpenAPI,预留空实现。
|
||||
// 后续接入平台用户中心 API 后补全同步逻辑。
|
||||
log.info("[getDeptSync] 部门同步暂未接入用户中心API,跳过");
|
||||
}
|
||||
|
||||
// ============================ GetSchModuleIsSms ============================
|
||||
|
||||
@Override
|
||||
public Boolean getSchModuleIsSms() {
|
||||
// C# 通过外部 ISchIsSmsBLL 查询学校短信开关。
|
||||
// Java 版暂不接入外部短信平台,默认返回 false。
|
||||
String account = NoticeContext.getAccount();
|
||||
if (account == null) {
|
||||
return false;
|
||||
}
|
||||
// 后续接入短信平台后补全查询逻辑
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 将前端 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
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.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.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务实现。
|
||||
* <p>
|
||||
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
|
||||
*/
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FileServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Value("${file.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
private static final String DEFAULT_APP_TYPE = "notice";
|
||||
|
||||
// ============================ 上传 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new NoticeException("上传文件不得为空");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
if (!StringUtils.hasText(originalName)) {
|
||||
throw new NoticeException("文件名为空");
|
||||
}
|
||||
// 提取后缀
|
||||
String ext = "";
|
||||
int dotIdx = originalName.lastIndexOf('.');
|
||||
if (dotIdx >= 0) {
|
||||
ext = originalName.substring(dotIdx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
|
||||
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
|
||||
|
||||
// 物理写入
|
||||
Path fullPath = Paths.get(uploadPath, relativePath);
|
||||
try {
|
||||
Files.createDirectories(fullPath.getParent());
|
||||
file.transferTo(fullPath.toFile());
|
||||
} catch (Exception e) {
|
||||
log.error("[upload] 文件保存失败: {}", fullPath, e);
|
||||
throw new NoticeException("文件保存失败", 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) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new NoticeException("文件不存在: " + fileId));
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
if (!Files.exists(fullPath)) {
|
||||
throw new NoticeException("物理文件不存在: " + file.getFileName());
|
||||
}
|
||||
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
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("[download] 文件下载失败: {}", file.getFileName(), e);
|
||||
throw new NoticeException("文件下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 删除 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer fileId) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new NoticeException("文件不存在: " + fileId));
|
||||
// 删物理文件
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
try {
|
||||
Files.deleteIfExists(fullPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
|
||||
}
|
||||
// 删数据库记录
|
||||
upFileRepository.delete(file);
|
||||
}
|
||||
|
||||
// ============================ 转换 ============================
|
||||
|
||||
private UpFileVo toVo(UpFile entity) {
|
||||
UpFileVo vo = new UpFileVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.common.event.NoticePublishedEvent;
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.entity.NoticeRead;
|
||||
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||
import com.yunhai.notice.enums.NoticeTypeEnum;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.*;
|
||||
import com.yunhai.notice.repository.NoticeReadRepository;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import com.yunhai.notice.utils.PageUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通知公告业务实现。
|
||||
*/
|
||||
@Service
|
||||
public class NoticeService implements INoticeService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoticeService.class);
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeReadRepository noticeReadRepository;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private static final int PUBLISHED = NoticeStatusEnum.PUBLISHED.getCode();
|
||||
private static final int DRAFT = NoticeStatusEnum.DRAFT.getCode();
|
||||
|
||||
// ============================ 发通知 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void publish(NoticeRequest request) {
|
||||
Notice entity = new Notice();
|
||||
BeanUtils.copyProperties(request, entity);
|
||||
entity.setId(null);
|
||||
// 定时发送:如果设置了未来时间,先存为草稿,由定时任务自动发布
|
||||
if (request.getTimingSendTime() != null && request.getTimingSendTime().isAfter(LocalDateTime.now())) {
|
||||
entity.setStatus(DRAFT);
|
||||
} else {
|
||||
entity.setStatus(PUBLISHED);
|
||||
}
|
||||
entity.setPublisherAccount(NoticeContext.getAccount());
|
||||
entity.setPublisherName(NoticeContext.getName());
|
||||
entity.setIsPulled(0);
|
||||
entity.setReadCount(0);
|
||||
noticeRepository.save(entity);
|
||||
|
||||
// 立即发布时触发消息推送事件
|
||||
if (entity.getStatus() == PUBLISHED) {
|
||||
publishEvent(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 保存草稿 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Integer saveDraft(NoticeRequest request) {
|
||||
Notice entity = new Notice();
|
||||
BeanUtils.copyProperties(request, entity);
|
||||
entity.setId(null);
|
||||
entity.setStatus(DRAFT);
|
||||
entity.setPublisherAccount(NoticeContext.getAccount());
|
||||
entity.setPublisherName(NoticeContext.getName());
|
||||
entity.setIsPulled(0);
|
||||
entity.setReadCount(0);
|
||||
noticeRepository.save(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
// ============================ 编辑 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void edit(Integer id, NoticeRequest request) {
|
||||
Objects.requireNonNull(id, "通知ID不得为空");
|
||||
Notice entity = noticeRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + id));
|
||||
// 发布人/状态保持不变,仅更新可编辑字段
|
||||
entity.setTitle(request.getTitle());
|
||||
entity.setContent(request.getContent());
|
||||
entity.setType(request.getType());
|
||||
entity.setDeptId(request.getDeptId());
|
||||
entity.setDeptName(request.getDeptName());
|
||||
entity.setTopmost(request.getTopmost());
|
||||
noticeRepository.save(entity);
|
||||
}
|
||||
|
||||
// ============================ 删除 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer id) {
|
||||
Objects.requireNonNull(id, "通知ID不得为空");
|
||||
if (!noticeRepository.existsById(id)) {
|
||||
throw new NoticeException("通知不存在: " + id);
|
||||
}
|
||||
// 级联清理阅读记录(逻辑外键,无物理 FK)
|
||||
noticeReadRepository.deleteByNoticeId(id);
|
||||
noticeRepository.deleteById(id);
|
||||
}
|
||||
|
||||
// ============================ 撤回 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void pull(Integer id) {
|
||||
Objects.requireNonNull(id, "通知ID不得为空");
|
||||
Notice entity = noticeRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + id));
|
||||
if (entity.getStatus() != PUBLISHED) {
|
||||
throw new NoticeException("仅已发布的通知可撤回");
|
||||
}
|
||||
if (entity.getIsPulled() != null && entity.getIsPulled() == 1) {
|
||||
throw new NoticeException("通知已被撤回,请勿重复操作");
|
||||
}
|
||||
entity.setIsPulled(1);
|
||||
noticeRepository.save(entity);
|
||||
}
|
||||
|
||||
// ============================ 置顶/取消置顶 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setTopmost(Integer id, Integer topmost) {
|
||||
Objects.requireNonNull(id, "通知ID不得为空");
|
||||
Notice entity = noticeRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + id));
|
||||
entity.setTopmost(topmost == null ? 0 : topmost);
|
||||
noticeRepository.save(entity);
|
||||
}
|
||||
|
||||
// ============================ 转发 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Integer forward(Integer sourceId, NoticeRequest request) {
|
||||
Objects.requireNonNull(sourceId, "源通知ID不得为空");
|
||||
Notice source = noticeRepository.findById(sourceId)
|
||||
.orElseThrow(() -> new NoticeException("源通知不存在: " + sourceId));
|
||||
Notice entity = new Notice();
|
||||
BeanUtils.copyProperties(request, entity);
|
||||
entity.setId(null);
|
||||
entity.setStatus(DRAFT);
|
||||
entity.setPublisherAccount(NoticeContext.getAccount());
|
||||
entity.setPublisherName(NoticeContext.getName());
|
||||
entity.setParentId(sourceId);
|
||||
entity.setIsPulled(0);
|
||||
entity.setReadCount(0);
|
||||
noticeRepository.save(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
// ============================ 发送通知(定时任务/手动发送) ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void sendNotify(Integer noticeId) {
|
||||
Objects.requireNonNull(noticeId, "通知ID不得为空");
|
||||
Notice entity = noticeRepository.findById(noticeId)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + noticeId));
|
||||
if (entity.getStatus() == PUBLISHED) {
|
||||
log.warn("[sendNotify] 通知 {} 已发布,跳过重复发送", noticeId);
|
||||
return;
|
||||
}
|
||||
entity.setStatus(PUBLISHED);
|
||||
noticeRepository.save(entity);
|
||||
publishEvent(entity);
|
||||
}
|
||||
|
||||
// ============================ 用户端列表(含未读) ============================
|
||||
|
||||
@Override
|
||||
public NoticeUserListResult userList(NoticeQuery query, Pageable pageable) {
|
||||
String account = NoticeContext.getAccount();
|
||||
Set<Integer> readIds = new HashSet<>(noticeReadRepository.findNoticeIdsByReaderAccount(account));
|
||||
|
||||
Page<Notice> page = noticeRepository.findAll(userSpec(query, readIds), pageable);
|
||||
|
||||
List<NoticeVo> content = page.getContent().stream()
|
||||
.map(n -> toVo(n, readIds.contains(n.getId())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long unreadCount = noticeRepository.countByStatus(PUBLISHED)
|
||||
- noticeReadRepository.countByReaderAccount(account);
|
||||
if (unreadCount < 0) {
|
||||
unreadCount = 0;
|
||||
}
|
||||
|
||||
return new NoticeUserListResult(content, (int) page.getTotalElements(),
|
||||
page.getNumber() + 1, page.getSize(), page.getTotalPages(), page.hasNext(), unreadCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户端动态查询:仅已发布;支持标题关键词、类型、部门、时间范围、未读/已读过滤。
|
||||
*/
|
||||
private Specification<Notice> userSpec(NoticeQuery query, Set<Integer> readIds) {
|
||||
return (Root<Notice> root, CriteriaQuery<?> cq, CriteriaBuilder cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
predicates.add(cb.equal(root.get("status"), PUBLISHED));
|
||||
appendCommonPredicates(query, root, cb, predicates);
|
||||
|
||||
Integer readType = query.getReadType() == null ? 0 : query.getReadType();
|
||||
if (readType == 1 && !readIds.isEmpty()) {
|
||||
predicates.add(cb.not(root.get("id").in(readIds)));
|
||||
} else if (readType == 2) {
|
||||
if (readIds.isEmpty()) {
|
||||
predicates.add(cb.equal(root.get("id"), -1)); // 无已读,结果集为空
|
||||
} else {
|
||||
predicates.add(root.get("id").in(readIds));
|
||||
}
|
||||
}
|
||||
return cq.where(predicates.toArray(new Predicate[0])).getRestriction();
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ 管理端列表(全部) ============================
|
||||
|
||||
@Override
|
||||
public BasePageResult<NoticeVo> adminList(NoticeQuery query, Pageable pageable) {
|
||||
Page<Notice> page = noticeRepository.findAll(adminSpec(query), pageable);
|
||||
List<NoticeVo> content = page.getContent().stream()
|
||||
.map(n -> toVo(n, false))
|
||||
.collect(Collectors.toList());
|
||||
return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端动态查询:按状态/标题/类型/部门/发布人/时间范围过滤。
|
||||
*/
|
||||
private Specification<Notice> adminSpec(NoticeQuery query) {
|
||||
return (Root<Notice> root, CriteriaQuery<?> cq, CriteriaBuilder cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (query.getStatus() != null) {
|
||||
predicates.add(cb.equal(root.get("status"), query.getStatus()));
|
||||
}
|
||||
appendCommonPredicates(query, root, cb, predicates);
|
||||
if (StringUtils.hasText(query.getPublisherAccount())) {
|
||||
predicates.add(cb.equal(root.get("publisherAccount"), query.getPublisherAccount()));
|
||||
}
|
||||
return cq.where(predicates.toArray(new Predicate[0])).getRestriction();
|
||||
};
|
||||
}
|
||||
|
||||
private void appendCommonPredicates(NoticeQuery query, Root<Notice> root, CriteriaBuilder cb,
|
||||
List<Predicate> predicates) {
|
||||
if (StringUtils.hasText(query.getKeyword())) {
|
||||
predicates.add(cb.like(root.get("title"), "%" + query.getKeyword() + "%"));
|
||||
}
|
||||
if (query.getType() != null) {
|
||||
predicates.add(cb.equal(root.get("type"), query.getType()));
|
||||
}
|
||||
if (StringUtils.hasText(query.getDeptId())) {
|
||||
predicates.add(cb.equal(root.get("deptId"), query.getDeptId()));
|
||||
}
|
||||
if (query.getStartTime() != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("createTime"), query.getStartTime()));
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
predicates.add(cb.lessThanOrEqualTo(root.get("createTime"), query.getEndTime()));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 详情(顺带标记已读) ============================
|
||||
|
||||
@Override
|
||||
public NoticeVo detail(Integer id) {
|
||||
Notice entity = noticeRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + id));
|
||||
String account = NoticeContext.getAccount();
|
||||
boolean read = noticeReadRepository.existsByNoticeIdAndReaderAccount(id, account);
|
||||
NoticeVo vo = toVo(entity, read);
|
||||
if (!read) {
|
||||
markRead(id);
|
||||
vo.setRead(true);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ============================ 标记已读 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void markRead(Integer id) {
|
||||
Objects.requireNonNull(id, "通知ID不得为空");
|
||||
String account = NoticeContext.getAccount();
|
||||
if (noticeReadRepository.existsByNoticeIdAndReaderAccount(id, account)) {
|
||||
return;
|
||||
}
|
||||
NoticeRead read = new NoticeRead();
|
||||
read.setNoticeId(id);
|
||||
read.setReaderAccount(account);
|
||||
read.setReaderName(NoticeContext.getName());
|
||||
read.setReadTime(LocalDateTime.now());
|
||||
noticeReadRepository.save(read);
|
||||
}
|
||||
|
||||
// ============================ 统计 ============================
|
||||
|
||||
@Override
|
||||
public NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end) {
|
||||
String account = NoticeContext.getAccount();
|
||||
LocalDateTime s = (start == null) ? LocalDateTime.of(1970, 1, 1, 0, 0) : start;
|
||||
LocalDateTime e = (end == null) ? LocalDateTime.now().plusDays(1) : end;
|
||||
|
||||
long total = noticeRepository.count();
|
||||
long publishedCount = noticeRepository.countByStatus(PUBLISHED);
|
||||
long draftCount = noticeRepository.countByStatus(NoticeStatusEnum.DRAFT.getCode());
|
||||
long totalReadRecords = noticeReadRepository.count();
|
||||
long unreadForCurrentUser = publishedCount - noticeReadRepository.countByReaderAccount(account);
|
||||
if (unreadForCurrentUser < 0) {
|
||||
unreadForCurrentUser = 0;
|
||||
}
|
||||
|
||||
// 按月统计(已发布)
|
||||
List<LocalDateTime> createTimes = noticeRepository
|
||||
.findCreateTimesByStatusAndRange(PUBLISHED, s, e);
|
||||
Map<String, Long> monthMap = new LinkedHashMap<>();
|
||||
for (LocalDateTime t : createTimes) {
|
||||
String key = t.getYear() + "-" + String.format("%02d", t.getMonthValue());
|
||||
monthMap.merge(key, 1L, Long::sum);
|
||||
}
|
||||
List<NoticeStatisticsVo.MonthStat> byMonth = monthMap.entrySet().stream()
|
||||
.map(en -> {
|
||||
String[] ym = en.getKey().split("-");
|
||||
return new NoticeStatisticsVo.MonthStat(
|
||||
Integer.parseInt(ym[0]), Integer.parseInt(ym[1]), en.getValue());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 按发布人统计(已发布)
|
||||
List<String> publishers = noticeRepository
|
||||
.findPublisherNamesByStatusAndRange(PUBLISHED, s, e);
|
||||
Map<String, Long> pubMap = new LinkedHashMap<>();
|
||||
for (String p : publishers) {
|
||||
pubMap.merge(p == null ? "未知" : p, 1L, Long::sum);
|
||||
}
|
||||
List<NoticeStatisticsVo.PublisherStat> byPublisher = pubMap.entrySet().stream()
|
||||
.map(en -> new NoticeStatisticsVo.PublisherStat(en.getKey(), en.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
NoticeStatisticsVo vo = new NoticeStatisticsVo();
|
||||
vo.setTotal(total);
|
||||
vo.setPublishedCount(publishedCount);
|
||||
vo.setDraftCount(draftCount);
|
||||
vo.setTotalReadRecords(totalReadRecords);
|
||||
vo.setUnreadForCurrentUser(unreadForCurrentUser);
|
||||
vo.setByPublisher(byPublisher);
|
||||
vo.setByMonth(byMonth);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ============================ 阅读明细 ============================
|
||||
|
||||
@Override
|
||||
public List<NoticeVo.ReadDetail> getReadDetail(Integer noticeId) {
|
||||
Objects.requireNonNull(noticeId, "通知ID不得为空");
|
||||
List<NoticeRead> readList = noticeReadRepository.findByNoticeIdOrderByReadTimeDesc(noticeId);
|
||||
return readList.stream().map(r -> {
|
||||
NoticeVo.ReadDetail detail = new NoticeVo.ReadDetail();
|
||||
detail.setAccount(r.getReaderAccount());
|
||||
detail.setName(r.getReaderName());
|
||||
detail.setRead(true);
|
||||
detail.setReadTime(r.getReadTime());
|
||||
return detail;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ 转换 ============================
|
||||
|
||||
private NoticeVo toVo(Notice entity, boolean read) {
|
||||
NoticeVo vo = new NoticeVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setTypeDesc(NoticeTypeEnum.of(entity.getType()).getDesc());
|
||||
vo.setStatusDesc(NoticeStatusEnum.of(entity.getStatus()).getDesc());
|
||||
vo.setRead(read);
|
||||
vo.setReadCount(noticeReadRepository.countByNoticeId(entity.getId()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ============================ 事件发布(异步推送消息) ============================
|
||||
|
||||
/**
|
||||
* 发布通知发布事件,由消息服务监听后在事务提交后异步推送(App/短信/企微/角标)。
|
||||
* 接收人列表从 receiveList(JSON数组)中解析;为空时表示全员通知。
|
||||
*/
|
||||
private void publishEvent(Notice notice) {
|
||||
List<String> receivers = parseReceiveList(notice.getReceiveList());
|
||||
NoticePublishedEvent event = new NoticePublishedEvent(
|
||||
notice.getId(), notice.getTitle(), receivers);
|
||||
eventPublisher.publishEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析接收人列表(JSON 数组字符串 -> 账号列表)。
|
||||
* 为空或解析失败时返回空列表(表示全员通知)。
|
||||
*/
|
||||
private List<String> parseReceiveList(String receiveListJson) {
|
||||
if (!StringUtils.hasText(receiveListJson)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
// 简单解析 JSON 数组 ["account1","account2"]
|
||||
String trimmed = receiveListJson.trim();
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
String inner = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||
if (inner.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] parts = inner.split(",");
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String part : parts) {
|
||||
String account = part.trim().replace("\"", "").replace("'", "");
|
||||
if (StringUtils.hasText(account)) {
|
||||
result.add(account);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[parseReceiveList] 解析接收人列表失败: {}", receiveListJson, e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
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.SmsService;
|
||||
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 SmsServiceImpl implements SmsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SmsServiceImpl.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.entity.NoticeTemplate;
|
||||
import com.yunhai.notice.entity.TemplateType;
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.repository.NoticeTemplateRepository;
|
||||
import com.yunhai.notice.repository.TemplateTypeRepository;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.TemplateService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通知模板服务实现。
|
||||
* 对应 C# TemplateController + Template.cs。
|
||||
*/
|
||||
@Service
|
||||
public class TemplateServiceImpl implements TemplateService {
|
||||
|
||||
@Autowired
|
||||
private TemplateTypeRepository templateTypeRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeTemplateRepository noticeTemplateRepository;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
// ============================ GetTypelist ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getTypelist() {
|
||||
List<TemplateType> types = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
// 首项固定为"全部"
|
||||
Map<String, Object> allItem = new LinkedHashMap<>();
|
||||
allItem.put("TypeID", 0);
|
||||
allItem.put("TypeName", "全部");
|
||||
result.add(allItem);
|
||||
|
||||
for (TemplateType t : types) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("TypeID", t.getId());
|
||||
item.put("TypeName", t.getTypeName());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ EditType ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void editType(Integer typeid, String typeName) {
|
||||
if (!StringUtils.hasText(typeName)) {
|
||||
throw new NoticeException("类型名称不能为空!");
|
||||
}
|
||||
// 重名校验
|
||||
boolean exists;
|
||||
if (typeid == null || typeid == 0) {
|
||||
exists = templateTypeRepository.existsByTypeName(typeName);
|
||||
} else {
|
||||
exists = templateTypeRepository.existsByTypeNameAndIdNot(typeName, typeid);
|
||||
}
|
||||
if (exists) {
|
||||
throw new NoticeException("类型名称已存在!");
|
||||
}
|
||||
|
||||
if (typeid == null || typeid == 0) {
|
||||
// 新增
|
||||
TemplateType type = new TemplateType();
|
||||
type.setTypeName(typeName);
|
||||
// sortOrder = max + 1
|
||||
List<TemplateType> all = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
int maxSort = all.stream()
|
||||
.mapToInt(t -> t.getSortOrder() != null ? t.getSortOrder() : 0)
|
||||
.max().orElse(0);
|
||||
type.setSortOrder(maxSort + 1);
|
||||
templateTypeRepository.save(type);
|
||||
} else {
|
||||
// 编辑
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new NoticeException("类型不存在!"));
|
||||
type.setTypeName(typeName);
|
||||
templateTypeRepository.save(type);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ DelType ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delType(Integer typeid) {
|
||||
if (typeid == null) {
|
||||
throw new NoticeException("类型不存在!");
|
||||
}
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new NoticeException("类型不存在!"));
|
||||
// 校验是否有模板引用此类型
|
||||
long refCount = noticeTemplateRepository.countByTypeId(typeid);
|
||||
if (refCount > 0) {
|
||||
throw new NoticeException("类型存在模板!");
|
||||
}
|
||||
templateTypeRepository.delete(type);
|
||||
}
|
||||
|
||||
// ============================ GetTemplatelist ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTemplatelist(Integer pageSize, Integer pageIndex, Integer typeid) {
|
||||
if (pageSize == null || pageSize <= 0) pageSize = 10;
|
||||
if (pageIndex == null || pageIndex <= 0) pageIndex = 1;
|
||||
if (typeid == null) typeid = 0;
|
||||
|
||||
Page<NoticeTemplate> page;
|
||||
int offset = pageIndex - 1;
|
||||
|
||||
if (typeid > 0) {
|
||||
page = noticeTemplateRepository.findByTypeId(typeid,
|
||||
PageRequest.of(offset, pageSize, Sort.by(Sort.Direction.DESC, "refCount")));
|
||||
} else {
|
||||
page = noticeTemplateRepository.findAllByOrderByRefCountDesc(PageRequest.of(offset, pageSize));
|
||||
}
|
||||
|
||||
// 加载类型名映射
|
||||
List<TemplateType> allTypes = templateTypeRepository.findAllByOrderBySortOrderAsc();
|
||||
Map<Integer, String> typeNameMap = allTypes.stream()
|
||||
.collect(Collectors.toMap(TemplateType::getId, TemplateType::getTypeName, (a, b) -> a));
|
||||
|
||||
// 加载封面图
|
||||
List<Integer> coverIds = page.getContent().stream()
|
||||
.map(NoticeTemplate::getCover)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Integer, String> coverUrlMap = new HashMap<>();
|
||||
if (!coverIds.isEmpty()) {
|
||||
for (Integer fid : coverIds) {
|
||||
upFileRepository.findById(fid).ifPresent(f -> coverUrlMap.put(fid, f.getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> listData = page.getContent().stream().map(t -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("SerID", t.getId());
|
||||
item.put("Title", t.getTitle());
|
||||
item.put("TypeName", typeNameMap.getOrDefault(t.getTypeId(), ""));
|
||||
item.put("Contents", t.getContent());
|
||||
item.put("ContentsFilter", filterHtml(t.getContent()));
|
||||
item.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
|
||||
item.put("imgurl", t.getCover() != null ? coverUrlMap.get(t.getCover()) : null);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", page.getTotalElements());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ EditTemplate ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover) {
|
||||
if (!StringUtils.hasText(title)) {
|
||||
throw new NoticeException("模板标题不能为空!");
|
||||
}
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new NoticeException("模板内容不能为空!");
|
||||
}
|
||||
|
||||
if (serID == null || serID == 0) {
|
||||
// 新增
|
||||
NoticeTemplate template = new NoticeTemplate();
|
||||
template.setTitle(title);
|
||||
template.setContent(content);
|
||||
template.setTypeId(typeId);
|
||||
template.setCover(cover);
|
||||
template.setRefCount(0);
|
||||
noticeTemplateRepository.save(template);
|
||||
} else {
|
||||
// 编辑
|
||||
NoticeTemplate template = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("模板不存在!"));
|
||||
template.setTitle(title);
|
||||
template.setContent(content);
|
||||
template.setTypeId(typeId);
|
||||
template.setCover(cover);
|
||||
noticeTemplateRepository.save(template);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ DelTemplate ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delTemplate(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new NoticeException("参数错误");
|
||||
}
|
||||
noticeTemplateRepository.findById(serID).ifPresent(noticeTemplateRepository::delete);
|
||||
}
|
||||
|
||||
// ============================ GetTemplateFist ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTemplateFist(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new NoticeException("参数错误");
|
||||
}
|
||||
NoticeTemplate t = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("模板不存在!"));
|
||||
|
||||
// 加载类型名
|
||||
String typeName = "";
|
||||
if (t.getTypeId() != null) {
|
||||
Optional<TemplateType> type = templateTypeRepository.findById(t.getTypeId());
|
||||
if (type.isPresent()) {
|
||||
typeName = type.get().getTypeName();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载封面图
|
||||
String imgurl = null;
|
||||
if (t.getCover() != null) {
|
||||
Optional<UpFile> file = upFileRepository.findById(t.getCover());
|
||||
if (file.isPresent()) {
|
||||
imgurl = file.get().getFileName();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("SerID", t.getId());
|
||||
result.put("Title", t.getTitle());
|
||||
result.put("Type", t.getTypeId());
|
||||
result.put("TypeName", typeName);
|
||||
result.put("Contents", t.getContent());
|
||||
result.put("Num", t.getRefCount() != null ? t.getRefCount() : 0);
|
||||
result.put("imgurl", imgurl);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 过滤HTML标签,返回纯文本(对应 C# WebHelper.FilterHtml)。
|
||||
*/
|
||||
private String filterHtml(String html) {
|
||||
if (html == null) return "";
|
||||
return html.replaceAll("<[^>]+>", "").trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yunhai.notice.utils;
|
||||
|
||||
import com.yunhai.notice.model.BasePageQuery;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
/**
|
||||
* 分页辅助类:BasePageQuery <-> Pageable,Page <-> BasePageResult。
|
||||
*/
|
||||
public class PageUtils {
|
||||
|
||||
private PageUtils() {
|
||||
}
|
||||
|
||||
public static Pageable toPageable(BasePageQuery query, Sort sort) {
|
||||
int page = Math.max(query.getPageIndex() - 1, 0);
|
||||
int size = Math.max(query.getPageSize(), 1);
|
||||
if (sort == null) {
|
||||
sort = Sort.unsorted();
|
||||
}
|
||||
return PageRequest.of(page, size, sort);
|
||||
}
|
||||
|
||||
public static <T> BasePageResult<T> toResult(Page<T> page) {
|
||||
return new BasePageResult<T>(
|
||||
page.getContent(),
|
||||
(int) page.getTotalElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.hasNext()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
server:
|
||||
port: 8070
|
||||
servlet:
|
||||
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:
|
||||
url: jdbc:mysql://localhost:3306/bsch_notice?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
min-idle: 10
|
||||
max-active: 30
|
||||
#jpa
|
||||
jpa:
|
||||
database: MySQL
|
||||
show-sql: true
|
||||
hibernate:
|
||||
# 开发期使用 update 自动建表;生产环境设置成 none
|
||||
ddl-auto: update
|
||||
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||
properties:
|
||||
hibernate.order_by.default_null_ordering: last
|
||||
open-in-view: true
|
||||
|
||||
#文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
#本地关闭 rabbit
|
||||
rabbitmq:
|
||||
listener:
|
||||
direct:
|
||||
auto-startup: false
|
||||
simple:
|
||||
auto-startup: false
|
||||
|
||||
# 文件上传存储路径(Docker 部署时与 volume 挂载一致)
|
||||
file:
|
||||
upload-path: D:/data/upload
|
||||
|
||||
# 定时发送任务
|
||||
timing-send:
|
||||
enabled: true
|
||||
cron: "0 * * * * ?" # 每分钟检查一次
|
||||
|
||||
# 短信模块配置
|
||||
sms:
|
||||
enabled: true
|
||||
module-id: notice
|
||||
|
||||
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
|
||||
yh:
|
||||
sso:
|
||||
server-url-prefix: http://sso.yunhaixingtong.com/cas/
|
||||
client-host-url: http://localhost:8070
|
||||
allow-headers: all
|
||||
allow-origins: all
|
||||
url-pattern: /api/*
|
||||
mobi-api-pattern: /mobiapi/*
|
||||
app-auth:
|
||||
enabled: true
|
||||
# 本应用自身身份(客户端签名用)
|
||||
app-key: 100001
|
||||
app-secret: ec4e860206374c80938719fff8e14a86
|
||||
open:
|
||||
enabled: true
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
user-center-url: http://base.yunhaixingtong.com/userbase
|
||||
@@ -0,0 +1,33 @@
|
||||
spring:
|
||||
application:
|
||||
name: notice
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
enabled: ${NACOS_CONFIG_ENABLED:false}
|
||||
server-addr: ${NACOS_ADDR:nacos.yunhaixingtong.com:8848}
|
||||
namespace: ${SPRING_CLOUD_NACOS_CONFIG_NAMESPACE:6196f0de-a28d-4894-8f10-e2982b57c6bc}
|
||||
# 应用自身配置所在分组
|
||||
group: OA_APP
|
||||
# 配置文件格式;最终主配置 dataId = ${spring.application.name}.${file-extension},即 notice.yaml
|
||||
file-extension: yaml
|
||||
encode: UTF-8
|
||||
# nacos 不可达时不强制退出,回退到本地 application.yml 兜底
|
||||
fail-fast: false
|
||||
# 只读账号
|
||||
username: ${SPRING_CLOUD_NACOS_USERNAME:yh-reader}
|
||||
password: ${SPRING_CLOUD_NACOS_PASSWORD:123456}
|
||||
# 共享公共配置:public 空间下的 common.properties
|
||||
shared-configs:
|
||||
# ① 1 号(最低优先级):public 命名空间(namespace="" 固定写空字符串)的 application-default.yaml
|
||||
- data-id: application-default.yaml
|
||||
group: DEFAULT_GROUP
|
||||
namespace: "" # ⭐ 固定 = "",强制指向 Nacos "public" 命名空间,不读上面的全局 prod namespace
|
||||
refresh: false
|
||||
|
||||
# ② 2 号(中等优先级):prod 命名空间(不写 namespace = 继承顶部全局 prod namespace id)的 application-infra.yaml
|
||||
- data-id: application-infra.yaml
|
||||
group: DEFAULT_GROUP
|
||||
refresh: false
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 引入 Spring Boot 默认彩色日志变量 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
|
||||
<!-- 控制台:所有环境都有,且带颜色2 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件日志:只有非 local 环境才启用 -->
|
||||
<springProfile name="!local">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/app/logs/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>5GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</springProfile>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<springProfile name="!local">
|
||||
<appender-ref ref="FILE" />
|
||||
</springProfile>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user