commit 40dd7fadea3ed1d2a00343a168eb589a5fd37577 Author: chenxing Date: Mon Aug 24 17:37:10 2026 +0800 init local code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a46b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.idea/ +*.iml +*.log +.DS_Store diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..19c6392 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,20 @@ +# Notice 后端镜像(基于 mvn package 产出的 target/notice.jar) +# 构建上下文 = service 目录: docker build -f service/docker/Dockerfile -t 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"] diff --git a/docker/application-prod.yml b/docker/application-prod.yml new file mode 100644 index 0000000..b0142bf --- /dev/null +++ b/docker/application-prod.yml @@ -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 diff --git a/docker/run-app.sh b/docker/run-app.sh new file mode 100644 index 0000000..1da625f --- /dev/null +++ b/docker/run-app.sh @@ -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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4909a92 --- /dev/null +++ b/pom.xml @@ -0,0 +1,215 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.yunhai + notice + 0.0.1 + Notice + 通知公告子应用 + + 1.8 + + + + + + + com.yunhai.platform + yh-platform-dependencies + 1.0.0-SNAPSHOT + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2021.0.5.0 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.security + spring-security-crypto + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.8 + + + + com.mysql + mysql-connector-j + runtime + + + + org.projectlombok + lombok + true + + + + + com.alibaba + fastjson + 1.2.80 + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + com.yunhai.platform + yh-auth-spring-boot-starter + + + com.yunhai.platform + yh-app-auth-spring-boot-starter + + + com.yunhai.platform + yh-openapi-spring-boot-starter + + + com.yunhai.platform + yh-api-std + + + + + org.jasig.cas.client + cas-client-core + 3.5.1 + + + + + com.alibaba + easyexcel + 3.3.2 + + + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + notice + + + org.apache.maven.plugins + maven-compiler-plugin + + + false + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.yunhai.notice.NoticeApplication + + + + + repackage + + + + + + + + + + + nexus-public + bsch/nexus public + http://nexus.yunhaixingtong.com/repository/maven-public/ + true + true + + + + + + ci + + C:/notice-build/notice + + + + + diff --git a/src/main/java/com/yunhai/notice/NoticeApplication.java b/src/main/java/com/yunhai/notice/NoticeApplication.java new file mode 100644 index 0000000..00971c2 --- /dev/null +++ b/src/main/java/com/yunhai/notice/NoticeApplication.java @@ -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); + } + +} diff --git a/src/main/java/com/yunhai/notice/common/GlobalExceptionHandler.java b/src/main/java/com/yunhai/notice/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..b247f5b --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/GlobalExceptionHandler.java @@ -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 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 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 handleConstraintViolation(ConstraintViolationException ex) { + log.warn("[ConstraintViolation] {}", ex.getMessage()); + return ApiResponse.fail(ErrorCode.PARAM_ERROR, ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.OK) + public ApiResponse 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); + } +} diff --git a/src/main/java/com/yunhai/notice/common/NoticeException.java b/src/main/java/com/yunhai/notice/common/NoticeException.java new file mode 100644 index 0000000..47f503f --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/NoticeException.java @@ -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); + } +} diff --git a/src/main/java/com/yunhai/notice/common/context/NoticeContext.java b/src/main/java/com/yunhai/notice/common/context/NoticeContext.java new file mode 100644 index 0000000..47749da --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/context/NoticeContext.java @@ -0,0 +1,32 @@ +package com.yunhai.notice.common.context; + +/** + * 当前登录用户上下文(ThreadLocal)。
+ * 由 {@link com.yunhai.notice.common.interceptor.CurrentUserInterceptor} 在请求入口写入, + * 供 JPA 审计监听器与业务层取用发布人/读者身份。

+ * 身份取自 CAS 写入的 {@code request.getRemoteUser()},本地未接入 CAS 时回退到 + * application.yml 的 dev.default-user 或请求头 X-Login-Account,便于本地联调。 + */ +public class NoticeContext { + + private static final ThreadLocal ACCOUNT = new ThreadLocal<>(); + private static final ThreadLocal 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(); + } +} diff --git a/src/main/java/com/yunhai/notice/common/event/NoticePublishedEvent.java b/src/main/java/com/yunhai/notice/common/event/NoticePublishedEvent.java new file mode 100644 index 0000000..268e41f --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/event/NoticePublishedEvent.java @@ -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 receiverAccounts; + + public NoticePublishedEvent(Integer noticeId, String title, List receiverAccounts) { + this.noticeId = noticeId; + this.title = title; + this.receiverAccounts = receiverAccounts; + } + + public Integer getNoticeId() { + return noticeId; + } + + public String getTitle() { + return title; + } + + public List getReceiverAccounts() { + return receiverAccounts; + } +} diff --git a/src/main/java/com/yunhai/notice/common/interceptor/CurrentUserInterceptor.java b/src/main/java/com/yunhai/notice/common/interceptor/CurrentUserInterceptor.java new file mode 100644 index 0000000..8a95140 --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/interceptor/CurrentUserInterceptor.java @@ -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(); + } +} diff --git a/src/main/java/com/yunhai/notice/common/listener/NoticeAuditListener.java b/src/main/java/com/yunhai/notice/common/listener/NoticeAuditListener.java new file mode 100644 index 0000000..5bcd6e9 --- /dev/null +++ b/src/main/java/com/yunhai/notice/common/listener/NoticeAuditListener.java @@ -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); + } + } +} diff --git a/src/main/java/com/yunhai/notice/config/Config.java b/src/main/java/com/yunhai/notice/config/Config.java new file mode 100644 index 0000000..6375822 --- /dev/null +++ b/src/main/java/com/yunhai/notice/config/Config.java @@ -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"; +} diff --git a/src/main/java/com/yunhai/notice/config/SwaggerConfig.java b/src/main/java/com/yunhai/notice/config/SwaggerConfig.java new file mode 100644 index 0000000..d264e0c --- /dev/null +++ b/src/main/java/com/yunhai/notice/config/SwaggerConfig.java @@ -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")); + } +} diff --git a/src/main/java/com/yunhai/notice/config/WebMvcConfig.java b/src/main/java/com/yunhai/notice/config/WebMvcConfig.java new file mode 100644 index 0000000..182b29e --- /dev/null +++ b/src/main/java/com/yunhai/notice/config/WebMvcConfig.java @@ -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"); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/BaseApiController.java b/src/main/java/com/yunhai/notice/controller/api/BaseApiController.java new file mode 100644 index 0000000..4173c55 --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/BaseApiController.java @@ -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}。 + *

+ * 这些接口在 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 getLayout() { + String account = NoticeContext.getAccount(); + String name = NoticeContext.getName(); + + Map 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> 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> 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()); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/ContactsController.java b/src/main/java/com/yunhai/notice/controller/api/ContactsController.java new file mode 100644 index 0000000..5e9468e --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/ContactsController.java @@ -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 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 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> getGroupTree(@RequestParam("type") Integer type) { + return contactsService.getGroupTree(type); + } + + // ============================ GetDeptRoleGroupTree ============================ + + /** + * 获取部门角色组树。 + * 前端: GET contacts/GetDeptRoleGroupTree + */ + @GetMapping("GetDeptRoleGroupTree") + public List> getDeptRoleGroupTree() { + return contactsService.getDeptRoleGroupTree(); + } + + // ============================ Import ============================ + + /** + * 导入联系组。 + * 前端: POST contacts/Import (Excel文件上传) + */ + @PostMapping("Import") + public Map importContacts() { + return contactsService.importContacts(); + } + + // ============================ Export ============================ + + /** + * 导出联系组。 + * 前端: GET contacts/Export + */ + @GetMapping("Export") + public Map exportContacts() { + return contactsService.exportContacts(); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/DictController.java b/src/main/java/com/yunhai/notice/controller/api/DictController.java new file mode 100644 index 0000000..262c57a --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/DictController.java @@ -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> getSysDic(@RequestParam(value = "type", defaultValue = "0") Integer type) { + return dictService.getSysDic(type); + } + + // ============================ GetDictMangeList ============================ + + /** + * 获取系统字典管理列表。 + * 前端: GET base/GetDictMangeList?type=dept + */ + @GetMapping("GetDictMangeList") + public List> 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(); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/FileController.java b/src/main/java/com/yunhai/notice/controller/api/FileController.java new file mode 100644 index 0000000..8eb2b6d --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/FileController.java @@ -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 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> fileUploadImage( + @RequestParam("file") MultipartFile[] files, + @RequestParam(value = "AppType", required = false) String appType) { + List> 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 fileList(@PathVariable Integer topicId) { + return fileService.listByTopic("notice", topicId); + } + + /** + * 批量绑定附件到通知。 + */ + @PostMapping("fileBind") + public void bindFiles(@RequestParam("fileIds") List fileIds, + @RequestParam("topicId") Integer topicId) { + fileService.bindToTopic(fileIds, topicId); + } + + private Map upFileToResult(UpFileVo vo) { + Map 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; + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/NoticeApiController.java b/src/main/java/com/yunhai/notice/controller/api/NoticeApiController.java new file mode 100644 index 0000000..3833bb8 --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/NoticeApiController.java @@ -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; + +/** + * 通知公告前端对接接口。 + *

匹配前端 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 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> list = result.getContent().stream().map(vo -> { + Map 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 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 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 result = noticeService.adminList(query, PageUtils.toPageable(query, sort)); + + List> list = result.getContent().stream().map(vo -> { + Map 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 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 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 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 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 fileIds = parseIds(chaoSend); + Page 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 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> orgizeModify(@RequestParam("serid") Integer serid) { + List readDetails = noticeService.getReadDetail(serid); + List> teaslist = readDetails.stream().map(d -> { + Map item = new LinkedHashMap<>(); + item.put("teaname", d.getName()); + item.put("states", "read"); + return item; + }).collect(Collectors.toList()); + + Map 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 readIds = noticeReadRepository.findNoticeIdsByReaderAccount(account); + Set readSet = new HashSet<>(readIds); + List 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 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 voToDetailMap(NoticeVo vo, Integer serid) { + Map 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 files = upFileRepository.findByTopicTypeAndTopicId("notice", serid); + List> chaoSend = files.stream().map(f -> { + Map 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 parseIds(String idsStr) { + List 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")); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/NoticeController.java b/src/main/java/com/yunhai/notice/controller/api/NoticeController.java new file mode 100644 index 0000000..ca5e06e --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/NoticeController.java @@ -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; + +/** + * 通知公告接口。 + *

路径前缀:/{platform}/base/(platform = api / manage / mobiapi), + * 与 UserBase BaseController 路径习惯一致,前端无需区分子应用。

+ */ +@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 getUserMod(BoaUser authUserWithDept) { + Integer schoolId = (authUserWithDept.getSchool() != null) ? authUserWithDept.getSchool().getId() : null; + List 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 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 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); + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/SmsController.java b/src/main/java/com/yunhai/notice/controller/api/SmsController.java new file mode 100644 index 0000000..e5e17d6 --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/SmsController.java @@ -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> getModuleList() { + return smsService.getModuleList(); + } + + // ============================ getgroup ============================ + + /** + * 获取学校父类组列表。 + * 前端: GET schsms/getgroup + */ + @GetMapping("getgroup") + public List> getSchParentGroup() { + return smsService.getSchParentGroup(); + } + + // ============================ getschsms ============================ + + /** + * 分页获取学校短信开关列表。 + * 前端: GET schsms/getschsms?PageSize=&PageIndex=&SchName= + */ + @GetMapping("getschsms") + public Map 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> 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; + } +} diff --git a/src/main/java/com/yunhai/notice/controller/api/TemplateController.java b/src/main/java/com/yunhai/notice/controller/api/TemplateController.java new file mode 100644 index 0000000..033bd23 --- /dev/null +++ b/src/main/java/com/yunhai/notice/controller/api/TemplateController.java @@ -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> 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 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 getTemplateFist(@RequestParam("serid") Integer serid) { + return templateService.getTemplateFist(serid); + } +} diff --git a/src/main/java/com/yunhai/notice/entity/BaseEntity.java b/src/main/java/com/yunhai/notice/entity/BaseEntity.java new file mode 100644 index 0000000..3583dcc --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/BaseEntity.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/ContactsGroup.java b/src/main/java/com/yunhai/notice/entity/ContactsGroup.java new file mode 100644 index 0000000..87e983e --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/ContactsGroup.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/ContactsGroupUser.java b/src/main/java/com/yunhai/notice/entity/ContactsGroupUser.java new file mode 100644 index 0000000..841c221 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/ContactsGroupUser.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/Notice.java b/src/main/java/com/yunhai/notice/entity/Notice.java new file mode 100644 index 0000000..2e1b5f8 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/Notice.java @@ -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(已剔除短信/模板/门户等无关字段)。 + *

+ * 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@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; +} diff --git a/src/main/java/com/yunhai/notice/entity/NoticeRead.java b/src/main/java/com/yunhai/notice/entity/NoticeRead.java new file mode 100644 index 0000000..754b892 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/NoticeRead.java @@ -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 的「逐用户阅读状态」语义, + * 但仅在读者首次查看时落库,避免发通知时预插全员。 + *

+ * 唯一约束 (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; +} diff --git a/src/main/java/com/yunhai/notice/entity/NoticeTemplate.java b/src/main/java/com/yunhai/notice/entity/NoticeTemplate.java new file mode 100644 index 0000000..1109854 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/NoticeTemplate.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/SmsModule.java b/src/main/java/com/yunhai/notice/entity/SmsModule.java new file mode 100644 index 0000000..f83f5e0 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/SmsModule.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/SmsSchoolConfig.java b/src/main/java/com/yunhai/notice/entity/SmsSchoolConfig.java new file mode 100644 index 0000000..3379825 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/SmsSchoolConfig.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/SysDict.java b/src/main/java/com/yunhai/notice/entity/SysDict.java new file mode 100644 index 0000000..59cff97 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/SysDict.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/TemplateType.java b/src/main/java/com/yunhai/notice/entity/TemplateType.java new file mode 100644 index 0000000..f5d386e --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/TemplateType.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/entity/UpFile.java b/src/main/java/com/yunhai/notice/entity/UpFile.java new file mode 100644 index 0000000..464af23 --- /dev/null +++ b/src/main/java/com/yunhai/notice/entity/UpFile.java @@ -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 表。 + *

+ * 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在 + * 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; +} diff --git a/src/main/java/com/yunhai/notice/enums/NoticeStatusEnum.java b/src/main/java/com/yunhai/notice/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..abceffd --- /dev/null +++ b/src/main/java/com/yunhai/notice/enums/NoticeStatusEnum.java @@ -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; + } +} diff --git a/src/main/java/com/yunhai/notice/enums/NoticeTypeEnum.java b/src/main/java/com/yunhai/notice/enums/NoticeTypeEnum.java new file mode 100644 index 0000000..587100d --- /dev/null +++ b/src/main/java/com/yunhai/notice/enums/NoticeTypeEnum.java @@ -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; + } +} diff --git a/src/main/java/com/yunhai/notice/job/TimingSendJob.java b/src/main/java/com/yunhai/notice/job/TimingSendJob.java new file mode 100644 index 0000000..936537e --- /dev/null +++ b/src/main/java/com/yunhai/notice/job/TimingSendJob.java @@ -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 的通知,自动发布。 + *

+ * 多实例部署时需加分布式锁(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 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); + } + } + } +} diff --git a/src/main/java/com/yunhai/notice/model/BasePageQuery.java b/src/main/java/com/yunhai/notice/model/BasePageQuery.java new file mode 100644 index 0000000..eb7b5a0 --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/BasePageQuery.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/model/BasePageResult.java b/src/main/java/com/yunhai/notice/model/BasePageResult.java new file mode 100644 index 0000000..660fccd --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/BasePageResult.java @@ -0,0 +1,33 @@ +package com.yunhai.notice.model; + +import lombok.Data; + +import java.util.List; + +/** + * 统一分页响应外壳。 + */ +@Data +public class BasePageResult { + /** 总记录数 */ + private int total; + /** 当前页数据 */ + private List content; + /** 当前页码(从 1 起) */ + private int pageIndex; + /** 每页条数 */ + private int pageSize; + /** 总页数 */ + private int totalPages; + /** 是否还有下一页 */ + private boolean hasNext; + + public BasePageResult(List 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; + } +} diff --git a/src/main/java/com/yunhai/notice/model/notice/NoticeQuery.java b/src/main/java/com/yunhai/notice/model/notice/NoticeQuery.java new file mode 100644 index 0000000..26a0f80 --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/NoticeQuery.java @@ -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; + +/** + * 通知列表查询条件。 + *

+ * - 管理端(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"; +} diff --git a/src/main/java/com/yunhai/notice/model/notice/NoticeRequest.java b/src/main/java/com/yunhai/notice/model/notice/NoticeRequest.java new file mode 100644 index 0000000..068f781 --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/NoticeRequest.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/model/notice/NoticeStatisticsVo.java b/src/main/java/com/yunhai/notice/model/notice/NoticeStatisticsVo.java new file mode 100644 index 0000000..3280fbf --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/NoticeStatisticsVo.java @@ -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 byPublisher; + + /** 按月统计(已发布通知数) */ + private List 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; + } + } +} diff --git a/src/main/java/com/yunhai/notice/model/notice/NoticeUserListResult.java b/src/main/java/com/yunhai/notice/model/notice/NoticeUserListResult.java new file mode 100644 index 0000000..0c049a4 --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/NoticeUserListResult.java @@ -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 content; + private int pageIndex; + private int pageSize; + private int totalPages; + private boolean hasNext; + + /** 当前用户未读通知总数 */ + private long unreadCount; + + public NoticeUserListResult(List 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; + } +} diff --git a/src/main/java/com/yunhai/notice/model/notice/NoticeVo.java b/src/main/java/com/yunhai/notice/model/notice/NoticeVo.java new file mode 100644 index 0000000..64f6c9c --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/NoticeVo.java @@ -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; + } +} diff --git a/src/main/java/com/yunhai/notice/model/notice/UpFileVo.java b/src/main/java/com/yunhai/notice/model/notice/UpFileVo.java new file mode 100644 index 0000000..b2b0c9e --- /dev/null +++ b/src/main/java/com/yunhai/notice/model/notice/UpFileVo.java @@ -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; +} diff --git a/src/main/java/com/yunhai/notice/repository/ContactsGroupRepository.java b/src/main/java/com/yunhai/notice/repository/ContactsGroupRepository.java new file mode 100644 index 0000000..9dc4f45 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/ContactsGroupRepository.java @@ -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 { + Page findAllByOrderByCreateTimeDesc(Pageable pageable); + + /** + * 按创建人账号查询联系组列表。 + */ + List findByCreatorAccount(String creatorAccount); + + /** + * 按创建人账号查询联系组列表(分页)。 + */ + Page findByCreatorAccount(String creatorAccount, Pageable pageable); + + /** + * 查询全部联系组(按创建时间降序)。 + */ + List findAllByOrderByCreateTimeDesc(); +} diff --git a/src/main/java/com/yunhai/notice/repository/ContactsGroupUserRepository.java b/src/main/java/com/yunhai/notice/repository/ContactsGroupUserRepository.java new file mode 100644 index 0000000..9f75871 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/ContactsGroupUserRepository.java @@ -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 { + Page findByGroupId(Integer groupId, Pageable pageable); + List findByGroupId(Integer groupId); + void deleteByGroupId(Integer groupId); + + /** + * 按多个组ID查询所有成员(用于统计每组人数、组树构建)。 + */ + List findByGroupIdIn(List groupIds); + + /** + * 按多个组ID批量删除成员。 + */ + void deleteByGroupIdIn(List groupIds); + + /** + * 按组ID和用户ID查询(判断是否已存在)。 + */ + boolean existsByGroupIdAndUserId(Integer groupId, Integer userId); +} diff --git a/src/main/java/com/yunhai/notice/repository/NoticeReadRepository.java b/src/main/java/com/yunhai/notice/repository/NoticeReadRepository.java new file mode 100644 index 0000000..56a060d --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/NoticeReadRepository.java @@ -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 { + + /** + * 某用户是否已读某通知。 + */ + 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 findNoticeIdsByReaderAccount(@Param("account") String account); + + /** + * 查询某用户对某通知的阅读记录(标记已读时幂等判断)。 + */ + Optional findByNoticeIdAndReaderAccount(Integer noticeId, String readerAccount); + + /** + * 查询某通知的全部阅读记录(按阅读时间倒序),用于阅读明细列表。 + */ + @Query("select r from NoticeRead r where r.noticeId = :noticeId order by r.readTime desc") + List findByNoticeIdOrderByReadTimeDesc(@Param("noticeId") Integer noticeId); +} diff --git a/src/main/java/com/yunhai/notice/repository/NoticeRepository.java b/src/main/java/com/yunhai/notice/repository/NoticeRepository.java new file mode 100644 index 0000000..62c1789 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/NoticeRepository.java @@ -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, + JpaSpecificationExecutor { + + /** + * 按状态计数(用于统计:已发布/草稿总数)。 + */ + long countByStatus(Integer status); + + /** + * 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。 + */ + @Query("select n.createTime from Notice n " + + "where n.status = :status and n.createTime >= :start and n.createTime <= :end") + List 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 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 findPendingTimingSend(@Param("status") Integer status, + @Param("now") LocalDateTime now); +} diff --git a/src/main/java/com/yunhai/notice/repository/NoticeTemplateRepository.java b/src/main/java/com/yunhai/notice/repository/NoticeTemplateRepository.java new file mode 100644 index 0000000..5d84fb8 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/NoticeTemplateRepository.java @@ -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 { + Page findByTypeId(Integer typeId, Pageable pageable); + + /** + * 查询全部模板(按引用次数降序),分页。 + */ + Page findAllByOrderByRefCountDesc(Pageable pageable); + + /** + * 按类型ID查询全部模板(不分页,用于检查类型是否被引用)。 + */ + long countByTypeId(Integer typeId); +} diff --git a/src/main/java/com/yunhai/notice/repository/SmsModuleRepository.java b/src/main/java/com/yunhai/notice/repository/SmsModuleRepository.java new file mode 100644 index 0000000..414b40a --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/SmsModuleRepository.java @@ -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 { + List findAllByOrderByIdAsc(); +} diff --git a/src/main/java/com/yunhai/notice/repository/SmsSchoolConfigRepository.java b/src/main/java/com/yunhai/notice/repository/SmsSchoolConfigRepository.java new file mode 100644 index 0000000..608590c --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/SmsSchoolConfigRepository.java @@ -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 { + Page findBySchoolNameContaining(String schoolName, Pageable pageable); + Page findAll(Pageable pageable); + List findBySchoolId(Integer schoolId); + List findBySchoolIdInAndModuleIdIn(List schoolIds, List moduleIds); + + /** + * 按学校ID和模块ID查询单条配置(判断是否已存在)。 + */ + SmsSchoolConfig findBySchoolIdAndModuleId(Integer schoolId, Integer moduleId); + + /** + * 按学校ID查询全部模块配置(用于"设置某校全部模块")。 + */ + List findBySchoolIdIn(List schoolIds); +} diff --git a/src/main/java/com/yunhai/notice/repository/SysDictRepository.java b/src/main/java/com/yunhai/notice/repository/SysDictRepository.java new file mode 100644 index 0000000..a122007 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/SysDictRepository.java @@ -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 { + List findByDictTypeOrderBySortOrderAsc(String dictType); + List findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag); +} diff --git a/src/main/java/com/yunhai/notice/repository/TemplateTypeRepository.java b/src/main/java/com/yunhai/notice/repository/TemplateTypeRepository.java new file mode 100644 index 0000000..d8abbd5 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/TemplateTypeRepository.java @@ -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 { + List findAllByOrderBySortOrderAsc(); + + /** + * 按类型名称查询(重名校验)。 + */ + boolean existsByTypeName(String typeName); + + /** + * 按类型名称查询(重名校验,排除自身)。 + */ + boolean existsByTypeNameAndIdNot(String typeName, Integer id); +} diff --git a/src/main/java/com/yunhai/notice/repository/UpFileRepository.java b/src/main/java/com/yunhai/notice/repository/UpFileRepository.java new file mode 100644 index 0000000..ad93ad4 --- /dev/null +++ b/src/main/java/com/yunhai/notice/repository/UpFileRepository.java @@ -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 { + + /** + * 按主题类型和主题ID查询附件列表。 + */ + List findByTopicTypeAndTopicId(String topicType, Integer topicId); + + /** + * 按主题类型和主题ID删除附件(通知删除时级联清理)。 + */ + void deleteByTopicTypeAndTopicId(String topicType, Integer topicId); +} diff --git a/src/main/java/com/yunhai/notice/service/ContactsService.java b/src/main/java/com/yunhai/notice/service/ContactsService.java new file mode 100644 index 0000000..39eb76a --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/ContactsService.java @@ -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 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 getContactsTea(String gid, Integer page, Integer count); + + /** + * 获取联系组树(组节点+用户节点,递归构建)。 + * + * @param type 类型(1 自定义组, 2 明信片) + * @return 树形结构 List<Map> + */ + List> getGroupTree(Integer type); + + /** + * 获取部门角色组树。 + * C# 从外部API获取部门树+用户角色映射。Java版预留。 + * + * @return 树形结构 List<Map> + */ + List> getDeptRoleGroupTree(); + + /** + * 导入联系组(Excel)。 + * C# 读取Excel文件,逐行匹配组名+教师。Java版预留。 + * + * @return 导入结果 Map + */ + Map importContacts(); + + /** + * 导出联系组(Excel)。 + * C# 生成Excel文件流返回。Java版预留。 + * + * @return 导出数据 Map + */ + Map exportContacts(); +} diff --git a/src/main/java/com/yunhai/notice/service/DictService.java b/src/main/java/com/yunhai/notice/service/DictService.java new file mode 100644 index 0000000..d48ac5e --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/DictService.java @@ -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> getSysDic(Integer type); + + /** + * 获取系统字典管理列表(含 SerID/CodeID/Name/Des/Enable)。 + * + * @param type 字典类型编号(0=部门类别) + * @return List<Map> + */ + List> 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(); +} diff --git a/src/main/java/com/yunhai/notice/service/FileService.java b/src/main/java/com/yunhai/notice/service/FileService.java new file mode 100644 index 0000000..e80ce30 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/FileService.java @@ -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 fileIds, Integer topicId); + + /** + * 查询某主题下的附件列表。 + * + * @param appType 应用类型 + * @param topicId 主题ID + * @return 附件列表 + */ + List listByTopic(String appType, Integer topicId); + + /** + * 下载文件。 + * + * @param fileId 文件记录ID + * @param response HTTP 响应 + */ + void download(Integer fileId, HttpServletResponse response); + + /** + * 删除文件(同时删除物理文件和数据库记录)。 + * + * @param fileId 文件记录ID + */ + void delete(Integer fileId); +} diff --git a/src/main/java/com/yunhai/notice/service/INoticeService.java b/src/main/java/com/yunhai/notice/service/INoticeService.java new file mode 100644 index 0000000..235df4a --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/INoticeService.java @@ -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 adminList(NoticeQuery query, Pageable pageable); + + /** + * 通知详情:返回视图对象,并顺带为当前用户标记已读(打开即视为已读)。 + */ + NoticeVo detail(Integer id); + + /** + * 标记已读:为当前用户写入(幂等)该通知的阅读记录。 + */ + void markRead(Integer id); + + /** + * 获取某通知的阅读明细列表。 + */ + List getReadDetail(Integer noticeId); + + /** + * 通知统计:总数/已发布/草稿、累计阅读人次、当前用户未读、按发布人、按月。 + */ + NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end); +} diff --git a/src/main/java/com/yunhai/notice/service/SmsService.java b/src/main/java/com/yunhai/notice/service/SmsService.java new file mode 100644 index 0000000..237f20b --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/SmsService.java @@ -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> getModuleList(); + + /** + * 获取学校父类组列表。 + * C# 从外部BLL获取学校组。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> getSchParentGroup(); + + /** + * 分页获取学校短信开关列表。 + * + * @param pageSize 每页条数 + * @param pageIndex 页码 + * @param schName 学校名称(模糊搜索) + * @return Map: { list, total } + */ + Map getSchIsSmsList(Integer pageSize, Integer pageIndex, String schName); + + /** + * 获取学校列表。 + * C# 从外部BLL获取学校列表。Java版预留。 + * + * @return List<Map> 每项 {ID, Name} + */ + List> 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); +} diff --git a/src/main/java/com/yunhai/notice/service/TemplateService.java b/src/main/java/com/yunhai/notice/service/TemplateService.java new file mode 100644 index 0000000..38578a6 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/TemplateService.java @@ -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> 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 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 getTemplateFist(Integer serID); +} diff --git a/src/main/java/com/yunhai/notice/service/impl/ContactsServiceImpl.java b/src/main/java/com/yunhai/notice/service/impl/ContactsServiceImpl.java new file mode 100644 index 0000000..e754424 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/ContactsServiceImpl.java @@ -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 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 getGroup(Integer page, Integer count, Integer type) { + if (type == null) { + type = 1; + } + String account = NoticeContext.getAccount(); + + // 查询当前用户创建的全部联系组 + List allGroups = groupRepository.findByCreatorAccount(account); + + // 统计每组人数 + List groupIds = allGroups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + Map countByGroup = allMembers.stream() + .collect(Collectors.groupingBy(ContactsGroupUser::getGroupId, Collectors.counting())); + + // 构建列表数据 + List> listData = allGroups.stream().map(g -> { + Map 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 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 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 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 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 getContactsTea(String gid, Integer page, Integer count) { + Integer groupId = 0; + try { + groupId = Integer.parseInt(gid); + } catch (NumberFormatException e) { + groupId = 0; + } + if (groupId == 0) { + Map empty = new LinkedHashMap<>(); + empty.put("list", Collections.emptyList()); + empty.put("total", 0); + return empty; + } + + List allMembers = groupUserRepository.findByGroupId(groupId); + int total = allMembers.size(); + + List> listData = allMembers.stream().map(m -> { + Map 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 result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", total); + return result; + } + + // ============================ GetGroupTree ============================ + + @Override + public List> getGroupTree(Integer type) { + if (type == null) { + type = 1; + } + String account = NoticeContext.getAccount(); + + // 查询当前用户的联系组 + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + + // 查询全部成员 + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + // 构建树节点 + List> treeNodes = new ArrayList<>(); + + // 组节点 (type=1, pid=0) + for (ContactsGroup g : groups) { + Map 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>()); + treeNodes.add(node); + } + + // 用户节点 (type=2, pid=groupId) + for (ContactsGroupUser m : allMembers) { + Map 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> tree = formatUserTree(treeNodes, 0); + + // 包裹根节点 + Map 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> getDeptRoleGroupTree() { + // C# 从外部API获取部门树+用户角色映射。 + // Java版暂未接入用户中心OpenAPI,返回空列表。 + log.info("[getDeptRoleGroupTree] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ Import ============================ + + @Override + @Transactional + public Map importContacts() { + // C# 读取上传的Excel文件逐行匹配组名+教师。 + // Java版暂未实现Excel解析,预留接口。 + log.info("[importContacts] Excel导入功能暂未实现"); + Map result = new LinkedHashMap<>(); + result.put("msg", "导入功能暂未实现"); + return result; + } + + // ============================ Export ============================ + + @Override + public Map exportContacts() { + // C# 生成Excel文件流返回。 + // Java版暂未实现Excel导出,预留接口。 + log.info("[exportContacts] Excel导出功能暂未实现"); + + // 返回联系组数据供前端参考 + String account = NoticeContext.getAccount(); + List groups = groupRepository.findByCreatorAccount(account); + List groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList()); + List allMembers = groupIds.isEmpty() ? Collections.emptyList() + : groupUserRepository.findByGroupIdIn(groupIds); + + List> rows = new ArrayList<>(); + for (ContactsGroup g : groups) { + List members = allMembers.stream() + .filter(m -> m.getGroupId().equals(g.getId())) + .collect(Collectors.toList()); + if (members.isEmpty()) { + Map row = new LinkedHashMap<>(); + row.put("组名", g.getGroupName()); + row.put("教师", ""); + row.put("用户名", ""); + rows.add(row); + } else { + for (ContactsGroupUser m : members) { + Map 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 result = new LinkedHashMap<>(); + result.put("list", rows); + return result; + } + + // ============================ 辅助方法 ============================ + + /** + * 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。 + */ + @SuppressWarnings("unchecked") + private List> formatUserTree(List> treeList, int pid) { + List> data; + if (pid == 0) { + // 根层:取没有父节点引用的节点(pid=0 或 pid不在列表中的) + Set 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> result = new ArrayList<>(); + for (Map item : data) { + int itemId = (Integer) item.get("id"); + int itemType = (Integer) item.get("type"); + + List> children = formatUserTree(treeList, itemId); + + // type=1(组) 且无子节点 → 不加入(C# 逻辑) + if (itemType == 1 && children.isEmpty()) { + continue; + } + + Map 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; + } +} diff --git a/src/main/java/com/yunhai/notice/service/impl/DictServiceImpl.java b/src/main/java/com/yunhai/notice/service/impl/DictServiceImpl.java new file mode 100644 index 0000000..b398642 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/DictServiceImpl.java @@ -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> getSysDic(Integer type) { + // C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及 + String dictType = mapDictType(type); + if (dictType == null) { + throw new NoticeException("参数错误"); + } + List 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 item = new LinkedHashMap<>(); + item.put("k", d.getDictKey()); + item.put("v", d.getDictValue()); + return item; + }) + .collect(Collectors.toList()); + } + + // ============================ GetDictMangeList ============================ + + @Override + public List> getDictMangeList(String type) { + // type="dept" 表示部门类别(DEPT) + String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null; + if (dictType == null) { + // 非 DEPT 类型需超管权限,本系统暂只支持 DEPT + throw new NoticeException("非超管不得操作!"); + } + List list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType); + return list.stream() + .sorted(Comparator.comparingInt(d -> d.getSortOrder() == null ? 999 : d.getSortOrder())) + .map(d -> { + Map 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 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 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 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 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; + } + } +} diff --git a/src/main/java/com/yunhai/notice/service/impl/FileServiceImpl.java b/src/main/java/com/yunhai/notice/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..98c0459 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/FileServiceImpl.java @@ -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; + +/** + * 文件上传/下载/删除业务实现。 + *

+ * 存储路径规则:{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 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 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; + } +} diff --git a/src/main/java/com/yunhai/notice/service/impl/NoticeService.java b/src/main/java/com/yunhai/notice/service/impl/NoticeService.java new file mode 100644 index 0000000..c272fcd --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/NoticeService.java @@ -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 readIds = new HashSet<>(noticeReadRepository.findNoticeIdsByReaderAccount(account)); + + Page page = noticeRepository.findAll(userSpec(query, readIds), pageable); + + List 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 userSpec(NoticeQuery query, Set readIds) { + return (Root root, CriteriaQuery cq, CriteriaBuilder cb) -> { + List 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 adminList(NoticeQuery query, Pageable pageable) { + Page page = noticeRepository.findAll(adminSpec(query), pageable); + List content = page.getContent().stream() + .map(n -> toVo(n, false)) + .collect(Collectors.toList()); + return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements())); + } + + /** + * 管理端动态查询:按状态/标题/类型/部门/发布人/时间范围过滤。 + */ + private Specification adminSpec(NoticeQuery query) { + return (Root root, CriteriaQuery cq, CriteriaBuilder cb) -> { + List 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 root, CriteriaBuilder cb, + List 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 createTimes = noticeRepository + .findCreateTimesByStatusAndRange(PUBLISHED, s, e); + Map monthMap = new LinkedHashMap<>(); + for (LocalDateTime t : createTimes) { + String key = t.getYear() + "-" + String.format("%02d", t.getMonthValue()); + monthMap.merge(key, 1L, Long::sum); + } + List 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 publishers = noticeRepository + .findPublisherNamesByStatusAndRange(PUBLISHED, s, e); + Map pubMap = new LinkedHashMap<>(); + for (String p : publishers) { + pubMap.merge(p == null ? "未知" : p, 1L, Long::sum); + } + List 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 getReadDetail(Integer noticeId) { + Objects.requireNonNull(noticeId, "通知ID不得为空"); + List 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 receivers = parseReceiveList(notice.getReceiveList()); + NoticePublishedEvent event = new NoticePublishedEvent( + notice.getId(), notice.getTitle(), receivers); + eventPublisher.publishEvent(event); + } + + /** + * 解析接收人列表(JSON 数组字符串 -> 账号列表)。 + * 为空或解析失败时返回空列表(表示全员通知)。 + */ + private List 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 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(); + } +} diff --git a/src/main/java/com/yunhai/notice/service/impl/SmsServiceImpl.java b/src/main/java/com/yunhai/notice/service/impl/SmsServiceImpl.java new file mode 100644 index 0000000..d8b88ff --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/SmsServiceImpl.java @@ -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> getModuleList() { + List modules = smsModuleRepository.findAllByOrderByIdAsc(); + return modules.stream().map(m -> { + Map item = new LinkedHashMap<>(); + item.put("ID", m.getId()); + item.put("Name", m.getModuleName()); + return item; + }).collect(Collectors.toList()); + } + + // ============================ getgroup ============================ + + @Override + public List> getSchParentGroup() { + // C# 从外部BLL获取学校组数据。Java版暂不接入用户中心API。 + // 返回空列表,前端可按需处理。 + log.info("[getSchParentGroup] 暂未接入用户中心API,返回空列表"); + return Collections.emptyList(); + } + + // ============================ getschsms ============================ + + @Override + public Map 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 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 modules = smsModuleRepository.findAllByOrderByIdAsc(); + + // 按 schoolId 分组配置 + Map> configBySchool = page.getContent().stream() + .collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId)); + + // 构建返回数据:每个学校一行,包含各模块的开关状态 + List> listData = page.getContent().stream() + .collect(Collectors.groupingBy(SmsSchoolConfig::getSchoolId)) + .entrySet().stream() + .map(entry -> { + Integer schoolId = entry.getKey(); + List configs = entry.getValue(); + String schoolName = configs.get(0).getSchoolName(); + + // 构建模块开关矩阵 + List> moduleList = new ArrayList<>(); + boolean isAll = true; + for (SmsModule module : modules) { + Map 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 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 result = new LinkedHashMap<>(); + result.put("list", listData); + result.put("total", page.getTotalElements()); + return result; + } + + // ============================ getsch ============================ + + @Override + public List> getSchList() { + // C# 从外部BLL获取学校列表。Java版从 sms_school_config 表提取已配置学校。 + // 获取所有不重复的学校 + List allConfigs = smsSchoolConfigRepository.findAll(); + Map schoolMap = new LinkedHashMap<>(); + for (SmsSchoolConfig config : allConfigs) { + schoolMap.putIfAbsent(config.getSchoolId(), config.getSchoolName()); + } + + return schoolMap.entrySet().stream().map(entry -> { + Map 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 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 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 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 schoolIds = Arrays.stream(schSerIDs.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + List 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 existing = smsSchoolConfigRepository.findBySchoolId(schoolId); + if (!existing.isEmpty()) { + config.setSchoolName(existing.get(0).getSchoolName()); + } + } + config.setIsSms(sms); + smsSchoolConfigRepository.save(config); + } + } + } +} diff --git a/src/main/java/com/yunhai/notice/service/impl/TemplateServiceImpl.java b/src/main/java/com/yunhai/notice/service/impl/TemplateServiceImpl.java new file mode 100644 index 0000000..6858d80 --- /dev/null +++ b/src/main/java/com/yunhai/notice/service/impl/TemplateServiceImpl.java @@ -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> getTypelist() { + List types = templateTypeRepository.findAllByOrderBySortOrderAsc(); + List> result = new ArrayList<>(); + + // 首项固定为"全部" + Map allItem = new LinkedHashMap<>(); + allItem.put("TypeID", 0); + allItem.put("TypeName", "全部"); + result.add(allItem); + + for (TemplateType t : types) { + Map 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 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 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 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 allTypes = templateTypeRepository.findAllByOrderBySortOrderAsc(); + Map typeNameMap = allTypes.stream() + .collect(Collectors.toMap(TemplateType::getId, TemplateType::getTypeName, (a, b) -> a)); + + // 加载封面图 + List coverIds = page.getContent().stream() + .map(NoticeTemplate::getCover) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + Map coverUrlMap = new HashMap<>(); + if (!coverIds.isEmpty()) { + for (Integer fid : coverIds) { + upFileRepository.findById(fid).ifPresent(f -> coverUrlMap.put(fid, f.getFileName())); + } + } + + List> listData = page.getContent().stream().map(t -> { + Map 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 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 getTemplateFist(Integer serID) { + if (serID == null) { + throw new NoticeException("参数错误"); + } + NoticeTemplate t = noticeTemplateRepository.findById(serID) + .orElseThrow(() -> new NoticeException("模板不存在!")); + + // 加载类型名 + String typeName = ""; + if (t.getTypeId() != null) { + Optional type = templateTypeRepository.findById(t.getTypeId()); + if (type.isPresent()) { + typeName = type.get().getTypeName(); + } + } + + // 加载封面图 + String imgurl = null; + if (t.getCover() != null) { + Optional file = upFileRepository.findById(t.getCover()); + if (file.isPresent()) { + imgurl = file.get().getFileName(); + } + } + + Map 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(); + } +} diff --git a/src/main/java/com/yunhai/notice/utils/PageUtils.java b/src/main/java/com/yunhai/notice/utils/PageUtils.java new file mode 100644 index 0000000..4ffff35 --- /dev/null +++ b/src/main/java/com/yunhai/notice/utils/PageUtils.java @@ -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 BasePageResult toResult(Page page) { + return new BasePageResult( + page.getContent(), + (int) page.getTotalElements(), + page.getNumber() + 1, + page.getSize(), + page.getTotalPages(), + page.hasNext() + ); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..0a6265d --- /dev/null +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..2724e1a --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -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 diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..20612de --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,35 @@ + + + + + + + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + + /app/logs/app.log + + /app/logs/app.%d{yyyy-MM-dd}.log + 30 + 5GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + + + + + + + + + + + \ No newline at end of file