通知修订
This commit is contained in:
@@ -3,14 +3,10 @@ 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) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.yunhai.notice.common;
|
||||
|
||||
import com.yunhai.platform.apistd.ApiResponse;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器:将业务异常转为统一响应 {@link ApiResponse}(code/msg/data)。
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(NoticeException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleNoticeException(NoticeException ex) {
|
||||
log.warn("[NoticeException] {}", ex.getMessage());
|
||||
String msg = ex.getMessage() == null ? "业务异常" : ex.getMessage();
|
||||
return ApiResponse.fail(5, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleValidException(MethodArgumentNotValidException ex) {
|
||||
String msg = ex.getBindingResult().getFieldError() != null
|
||||
? ex.getBindingResult().getFieldError().getDefaultMessage()
|
||||
: "参数校验失败";
|
||||
log.warn("[Validation] {}", msg);
|
||||
return ApiResponse.fail(ErrorCode.PARAM_ERROR, msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleConstraintViolation(ConstraintViolationException ex) {
|
||||
log.warn("[ConstraintViolation] {}", ex.getMessage());
|
||||
return ApiResponse.fail(ErrorCode.PARAM_ERROR, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public ApiResponse<Object> handleException(Exception ex) {
|
||||
log.error("[UnhandledException] ", ex);
|
||||
String msg = ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage();
|
||||
return ApiResponse.fail(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "服务器内部错误:" + msg);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.yunhai.notice.common.context;
|
||||
|
||||
/**
|
||||
* 当前登录用户上下文(ThreadLocal)。<br>
|
||||
* 由 {@link com.yunhai.notice.common.interceptor.CurrentUserInterceptor} 在请求入口写入,
|
||||
* 供 JPA 审计监听器与业务层取用发布人/读者身份。<br><br>
|
||||
* 身份取自 CAS 写入的 {@code request.getRemoteUser()},本地未接入 CAS 时回退到
|
||||
* application.yml 的 dev.default-user 或请求头 X-Login-Account,便于本地联调。
|
||||
*/
|
||||
public class NoticeContext {
|
||||
|
||||
private static final ThreadLocal<String> ACCOUNT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> NAME = new ThreadLocal<>();
|
||||
|
||||
public static void set(String account, String name) {
|
||||
ACCOUNT.set(account);
|
||||
NAME.set(name);
|
||||
}
|
||||
|
||||
public static String getAccount() {
|
||||
return ACCOUNT.get();
|
||||
}
|
||||
|
||||
public static String getName() {
|
||||
return NAME.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
ACCOUNT.remove();
|
||||
NAME.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.yunhai.notice.common.event;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知发布事件。在通知状态从草稿变更为已发布时发布,
|
||||
* 由 {@code MessageService} 监听并在事务提交后异步推送消息(短信/App/角标等)。
|
||||
*/
|
||||
public class NoticePublishedEvent {
|
||||
|
||||
private final Integer noticeId;
|
||||
private final String title;
|
||||
private final List<String> receiverAccounts;
|
||||
|
||||
public NoticePublishedEvent(Integer noticeId, String title, List<String> receiverAccounts) {
|
||||
this.noticeId = noticeId;
|
||||
this.title = title;
|
||||
this.receiverAccounts = receiverAccounts;
|
||||
}
|
||||
|
||||
public Integer getNoticeId() {
|
||||
return noticeId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<String> getReceiverAccounts() {
|
||||
return receiverAccounts;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.yunhai.notice.common.listener;
|
||||
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.entity.BaseEntity;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.persistence.PrePersist;
|
||||
@@ -9,8 +10,9 @@ import javax.persistence.PreUpdate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(登录账号)与时间戳。
|
||||
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(平台当前登录人)与时间戳。
|
||||
* 对应 {@link BaseEntity} 的 createBy / updateBy / createTime / updateTime 字段。
|
||||
* 当前登录人取自基础平台 {@link UserContextHolder}。
|
||||
*/
|
||||
@Component
|
||||
public class NoticeAuditListener {
|
||||
@@ -20,7 +22,7 @@ public class NoticeAuditListener {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
if (account != null) {
|
||||
if (entity.getCreateBy() == null) {
|
||||
entity.setCreateBy(account);
|
||||
@@ -32,9 +34,14 @@ public class NoticeAuditListener {
|
||||
@PreUpdate
|
||||
public void preUpdate(BaseEntity entity) {
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
if (account != null) {
|
||||
entity.setUpdateBy(account);
|
||||
}
|
||||
}
|
||||
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yunhai.notice.common.validation;
|
||||
|
||||
/**
|
||||
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
|
||||
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
|
||||
*/
|
||||
public interface ValidationGroups {
|
||||
interface Create {}
|
||||
interface Update {}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,110 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.notice.service.IFileService;
|
||||
import com.yunhai.notice.service.IUserCenterService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 基础平台代理接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/base/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* <p>
|
||||
* 这些接口在 C# 原版中依赖用户中心 OpenAPI(BaseOpenApi/HttpUserApi)获取数据,
|
||||
* Java 版暂以桩实现返回,后续接入平台用户中心 API 后补全。
|
||||
* 平台代理域接口(base/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/base/"}(原硬编码已修正)。
|
||||
* 承载平台代理接口:GetUserInfo / GetUserMod / GetSchoolAll / GetGroupUser / Logout,
|
||||
* 业务逻辑已下沉 {@link IUserCenterService},本控制器仅做"取参 → 调 Service → 返回"。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||
public class BaseApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BaseApiController.class);
|
||||
|
||||
@Value("${notice.app-name:通知公告}")
|
||||
private String appName;
|
||||
@Autowired
|
||||
private IUserCenterService userCenterService;
|
||||
|
||||
// ============================ GetLayout ============================
|
||||
@Autowired
|
||||
private IFileService fileService;
|
||||
|
||||
// ============================ FileUpload ============================
|
||||
|
||||
/**
|
||||
* 获取布局数据(模块+头部)。
|
||||
* 前端: GET base/GetLayout
|
||||
* C# 通过 OpenAPI 获取用户模块权限列表。
|
||||
* Java 版返回基础布局信息,pathList 待接入用户中心后补全。
|
||||
* 附件上传(单文件)。
|
||||
* 前端: POST base/FileUpload (form-data: file)
|
||||
* 返回: { fileid, host, fileurl, name, type, size }
|
||||
*/
|
||||
@GetMapping("GetLayout")
|
||||
public Map<String, Object> getLayout() {
|
||||
String account = NoticeContext.getAccount();
|
||||
String name = NoticeContext.getName();
|
||||
@PostMapping("FileUpload")
|
||||
public UpFileResultVo fileUpload(@RequestParam("file") MultipartFile file) {
|
||||
return fileService.uploadToResult(file, "notice");
|
||||
}
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("ssoUrl", "");
|
||||
data.put("username", name != null ? name : (account != null ? account : ""));
|
||||
data.put("pathList", Collections.emptyList()); // 待接入用户中心API
|
||||
data.put("topName", appName);
|
||||
data.put("pageUrl", "");
|
||||
data.put("logoUrl", "");
|
||||
data.put("initUrl", "");
|
||||
data.put("id", 0);
|
||||
data.put("avatar", "");
|
||||
return data;
|
||||
// ============================ GetUserInfo ============================
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息(CAS 会话注入)。
|
||||
*/
|
||||
@GetMapping("GetUserInfo")
|
||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||
return authUserWithDept;
|
||||
}
|
||||
|
||||
// ============================ GetUserMod ============================
|
||||
|
||||
/**
|
||||
* 获取当前用户已授权的模块列表(左侧菜单树)。
|
||||
* 模块树组装已下沉 IUserCenterService。
|
||||
*/
|
||||
@GetMapping("GetUserMod")
|
||||
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
return userCenterService.getUserModuleTree(authUserWithDept.getId(), schId);
|
||||
}
|
||||
|
||||
// ============================ GetSchoolAll ============================
|
||||
|
||||
/**
|
||||
* 获取当前用户学校组及用户(树形结构)。
|
||||
* 前端: GET base/GetSchoolAll?type=
|
||||
* C# 通过 ISchGroupBLL + ITeainfoBLL 获取学校组+教师树。
|
||||
* Java 版待接入用户中心API,暂返回空树。
|
||||
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
|
||||
*/
|
||||
@GetMapping("GetSchoolAll")
|
||||
public List<Map<String, Object>> getSchoolAll(@RequestParam(value = "type", defaultValue = "") String type) {
|
||||
log.info("[getSchoolAll] 暂未接入用户中心API,返回空列表");
|
||||
return Collections.emptyList();
|
||||
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
|
||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||
return userCenterService.getSchoolAllTree(schId);
|
||||
}
|
||||
|
||||
// ============================ GetGroupUser ============================
|
||||
|
||||
/**
|
||||
* 获取当前组用户。
|
||||
* 前端: GET base/GetGroupUser?gid=&role=
|
||||
* C# 通过 ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo 获取组内教师+管理员。
|
||||
* Java 版待接入用户中心API,暂返回空列表。
|
||||
* 获取指定组内用户(教师 + 管理员)。
|
||||
* 空桩已改为显式报错(避免前端误以为组内无用户),由 IUserCenterService 统一处理。
|
||||
*/
|
||||
@GetMapping("GetGroupUser")
|
||||
public List<Map<String, Object>> getGroupUser(
|
||||
@RequestParam("gid") Integer gid,
|
||||
@RequestParam(value = "role", defaultValue = "2") Integer role) {
|
||||
log.info("[getGroupUser] gid={}, 暂未接入用户中心API,返回空列表", gid);
|
||||
return Collections.emptyList();
|
||||
return userCenterService.getGroupUser(gid, role);
|
||||
}
|
||||
|
||||
// ============================ Logout ============================
|
||||
|
||||
/**
|
||||
* 退出登录。
|
||||
* 前端: POST base/Logout
|
||||
* C# 清除 Session + FormsAuth。
|
||||
* Java 版由 Spring Security/平台网关处理认证,此处仅返回成功。
|
||||
* 认证由平台网关处理,此处仅返回成功。
|
||||
*/
|
||||
@PostMapping("Logout")
|
||||
public void logout() {
|
||||
// 认证由平台网关处理,此处无需额外操作
|
||||
log.info("[logout] 用户退出登录: {}", NoticeContext.getAccount());
|
||||
LoginUser user = UserContextHolder.get();
|
||||
log.info("[logout] 用户退出登录: {}", user == null ? null : user.getLoginName());
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,27 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.ContactsService;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.service.IContactsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 联系组接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/contacts/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 联系组接口(contacts/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/contacts/"}(原硬编码已修正为 Config 拼接)。
|
||||
* 方法名已为 PascalCase 且写操作已是 POST,保持不动。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/contacts/")
|
||||
@RequestMapping(value = Config.API_PATH + "/contacts/")
|
||||
public class ContactsController {
|
||||
|
||||
@Autowired
|
||||
private ContactsService contactsService;
|
||||
|
||||
// ============================ AddGroup ============================
|
||||
private IContactsService contactsService;
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
* 前端: POST contacts/AddGroup (form-urlencoded: name, type)
|
||||
*/
|
||||
@PostMapping("AddGroup")
|
||||
public void addGroup(
|
||||
@@ -32,11 +30,8 @@ public class ContactsController {
|
||||
contactsService.addGroup(name, Integer.parseInt(type));
|
||||
}
|
||||
|
||||
// ============================ GetGroup ============================
|
||||
|
||||
/**
|
||||
* 获取联系组列表。
|
||||
* 前端: GET contacts/GetGroup?page=&count=&type=
|
||||
*/
|
||||
@GetMapping("GetGroup")
|
||||
public Map<String, Object> getGroup(
|
||||
@@ -46,11 +41,8 @@ public class ContactsController {
|
||||
return contactsService.getGroup(page, count, type);
|
||||
}
|
||||
|
||||
// ============================ EditGroup ============================
|
||||
|
||||
/**
|
||||
* 编辑联系组。
|
||||
* 前端: POST contacts/EditGroup (form-urlencoded: name, id, type)
|
||||
*/
|
||||
@PostMapping("EditGroup")
|
||||
public void editGroup(
|
||||
@@ -60,22 +52,16 @@ public class ContactsController {
|
||||
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(
|
||||
@@ -84,11 +70,8 @@ public class ContactsController {
|
||||
contactsService.setContactsTea(Integer.parseInt(gid), ids);
|
||||
}
|
||||
|
||||
// ============================ GetContactsTea ============================
|
||||
|
||||
/**
|
||||
* 获取联系组教师列表。
|
||||
* 前端: GET contacts/GetContactsTea?gid=&page=&count=
|
||||
*/
|
||||
@GetMapping("GetContactsTea")
|
||||
public Map<String, Object> getContactsTea(
|
||||
@@ -98,47 +81,19 @@ public class ContactsController {
|
||||
return contactsService.getContactsTea(gid, page, count);
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
/**
|
||||
* 获取联系组树。
|
||||
* 前端: GET contacts/GetGroupTree?type=
|
||||
*/
|
||||
@GetMapping("GetGroupTree")
|
||||
public List<Map<String, Object>> getGroupTree(@RequestParam("type") Integer type) {
|
||||
return contactsService.getGroupTree(type);
|
||||
}
|
||||
|
||||
// ============================ GetDeptRoleGroupTree ============================
|
||||
|
||||
/**
|
||||
* 获取部门角色组树。
|
||||
* 前端: GET contacts/GetDeptRoleGroupTree
|
||||
*/
|
||||
@GetMapping("GetDeptRoleGroupTree")
|
||||
public List<Map<String, Object>> getDeptRoleGroupTree() {
|
||||
return contactsService.getDeptRoleGroupTree();
|
||||
}
|
||||
|
||||
// ============================ Import ============================
|
||||
|
||||
/**
|
||||
* 导入联系组。
|
||||
* 前端: POST contacts/Import (Excel文件上传)
|
||||
*/
|
||||
@PostMapping("Import")
|
||||
public Map<String, Object> importContacts() {
|
||||
return contactsService.importContacts();
|
||||
}
|
||||
|
||||
// ============================ Export ============================
|
||||
|
||||
/**
|
||||
* 导出联系组。
|
||||
* 前端: GET contacts/Export
|
||||
*/
|
||||
@GetMapping("Export")
|
||||
public Map<String, Object> exportContacts() {
|
||||
return contactsService.exportContacts();
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,81 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.DictService;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.service.IDictService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统字典接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/base/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 系统字典接口(发文部门域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/dict/"}(原 base/ 前缀已修正)。
|
||||
* 写操作一律 POST(原 DelDept/OrdbyDicIds 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
@RequestMapping(value = Config.API_PATH + "/dict/")
|
||||
public class DictController {
|
||||
|
||||
@Autowired
|
||||
private DictService dictService;
|
||||
|
||||
// ============================ GetSysDic ============================
|
||||
private IDictService dictService;
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
* 前端: GET base/GetSysDic?Type=1
|
||||
*/
|
||||
@GetMapping("GetSysDic")
|
||||
public List<Map<String, Object>> getSysDic(@RequestParam(value = "type", defaultValue = "0") Integer type) {
|
||||
@GetMapping("List")
|
||||
public List<Map<String, Object>> list(@RequestParam(value = "type", defaultValue = "0") Integer type) {
|
||||
return dictService.getSysDic(type);
|
||||
}
|
||||
|
||||
// ============================ GetDictMangeList ============================
|
||||
|
||||
/**
|
||||
* 获取系统字典管理列表。
|
||||
* 前端: GET base/GetDictMangeList?type=dept
|
||||
*/
|
||||
@GetMapping("GetDictMangeList")
|
||||
public List<Map<String, Object>> getDictMangeList(@RequestParam(value = "type", defaultValue = "dept") String type) {
|
||||
@GetMapping("ManageList")
|
||||
public List<Map<String, Object>> manageList(@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) {
|
||||
@PostMapping("Add")
|
||||
public void add(@RequestParam("name") String name) {
|
||||
dictService.addDeptDic(name);
|
||||
}
|
||||
|
||||
// ============================ UpdateDeptDic ============================
|
||||
|
||||
/**
|
||||
* 更新发文单位。
|
||||
* 前端: POST base/UpdateDeptDic (form-urlencoded: SerID, Name, Enable)
|
||||
*/
|
||||
@PostMapping("UpdateDeptDic")
|
||||
public void updateDeptDic(
|
||||
@PostMapping("Update")
|
||||
public void update(
|
||||
@RequestParam("SerID") Integer serID,
|
||||
@RequestParam("Name") String name,
|
||||
@RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable) {
|
||||
dictService.updateDeptDic(serID, name, enable);
|
||||
}
|
||||
|
||||
// ============================ DelDept ============================
|
||||
|
||||
/**
|
||||
* 删除发文单位。
|
||||
* 前端: GET base/DelDept?serid=xxx
|
||||
* 删除发文单位(原 GET DelDept,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("DelDept")
|
||||
public void delDept(@RequestParam("serid") Integer serID) {
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestParam("serid") Integer serID) {
|
||||
dictService.delDept(serID);
|
||||
}
|
||||
|
||||
// ============================ OrdbyDicIds ============================
|
||||
|
||||
/**
|
||||
* 发文单位排序。
|
||||
* 前端: GET base/OrdbyDicIds?ids=1,2,3
|
||||
* 发文单位排序(原 GET OrdbyDicIds,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("OrdbyDicIds")
|
||||
public void ordbyDicIds(@RequestParam("ids") String ids) {
|
||||
@PostMapping("Sort")
|
||||
public void sort(@RequestParam("ids") String ids) {
|
||||
dictService.ordbyDicIds(ids);
|
||||
}
|
||||
|
||||
// ============================ GetDeptSync ============================
|
||||
|
||||
/**
|
||||
* 发文单位同步。
|
||||
* 前端: GET base/GetDeptSync
|
||||
*/
|
||||
@GetMapping("GetDeptSync")
|
||||
public void getDeptSync() {
|
||||
@GetMapping("Sync")
|
||||
public void sync() {
|
||||
dictService.getDeptSync();
|
||||
}
|
||||
|
||||
// ============================ GetSchModuleIsSms ============================
|
||||
|
||||
/**
|
||||
* 是否发送短信。
|
||||
* 前端: GET base/GetSchModuleIsSms
|
||||
*/
|
||||
@GetMapping("GetSchModuleIsSms")
|
||||
public Boolean getSchModuleIsSms() {
|
||||
return dictService.getSchModuleIsSms();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,88 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
import com.yunhai.notice.service.IFileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件接口(兼容前端 Notice3.0 URL)。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 附件域接口(file/)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。
|
||||
* 方法名统一 PascalCase;写操作一律 POST。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/base/")
|
||||
@RequestMapping(value = Config.API_PATH + "/file/")
|
||||
public class FileController {
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
private IFileService fileService;
|
||||
|
||||
/**
|
||||
* 附件上传(单文件)。
|
||||
* 前端: POST base/FileUpload (form-data, 含 AppType)
|
||||
* 前端: POST file/Upload (form-data)
|
||||
* 返回: { fileid, host, fileurl, name, type, size }
|
||||
*/
|
||||
@PostMapping("FileUpload")
|
||||
public Map<String, Object> fileUpload(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "AppType", required = false) String appType) {
|
||||
UpFileVo vo = fileService.upload(file, appType != null ? appType : "notice", null);
|
||||
return upFileToResult(vo);
|
||||
@PostMapping("Upload")
|
||||
public UpFileResultVo upload(@RequestParam("file") MultipartFile file) {
|
||||
return fileService.uploadToResult(file, "notice");
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(多文件,返回数组)。
|
||||
* 前端: POST base/FileUploadImage (form-data, accept=image/*)
|
||||
* 返回: [{ fileid, host, fileurl, name, type, size }]
|
||||
* 前端: POST file/UploadImage (form-data, accept=image/*)
|
||||
*/
|
||||
@PostMapping("FileUploadImage")
|
||||
public List<Map<String, Object>> fileUploadImage(
|
||||
@RequestParam("file") MultipartFile[] files,
|
||||
@RequestParam(value = "AppType", required = false) String appType) {
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
UpFileVo vo = fileService.upload(file, appType != null ? appType : "notice", null);
|
||||
results.add(upFileToResult(vo));
|
||||
}
|
||||
return results;
|
||||
@PostMapping("UploadImage")
|
||||
public List<UpFileVo> uploadImage(@RequestParam("file") MultipartFile[] files) {
|
||||
return fileService.uploadImages(files, "notice");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载(浏览器直接 location.href 跳转)。
|
||||
* 前端: GET base/FileDown?id=xxx
|
||||
*/
|
||||
@GetMapping("FileDown")
|
||||
public void fileDown(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||
@GetMapping("Down")
|
||||
public void down(@RequestParam("id") Integer id, HttpServletResponse response) {
|
||||
fileService.download(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件预览。
|
||||
* 前端: GET base/FilePreview?id=xxx&type=2
|
||||
* 文件在线预览(inline 输出,前端取 Blob 交给 open-file-viewer 渲染)。
|
||||
*/
|
||||
@GetMapping("FilePreview")
|
||||
public void filePreview(@RequestParam("id") Integer id,
|
||||
@RequestParam(value = "type", required = false) Integer type,
|
||||
HttpServletResponse response) {
|
||||
fileService.download(id, response);
|
||||
@GetMapping("Preview")
|
||||
public void preview(@RequestParam("id") Integer id,
|
||||
@RequestParam(value = "type", required = false) Integer type,
|
||||
HttpServletResponse response) {
|
||||
fileService.preview(id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件。
|
||||
*/
|
||||
@PostMapping("fileDelete/{fileId}")
|
||||
public void delete(@PathVariable Integer fileId) {
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestParam("fileId") Integer fileId) {
|
||||
fileService.delete(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某通知下的附件列表。
|
||||
*/
|
||||
@GetMapping("fileList/{topicId}")
|
||||
public List<UpFileVo> fileList(@PathVariable Integer topicId) {
|
||||
@GetMapping("List")
|
||||
public List<UpFileVo> list(@RequestParam("topicId") Integer topicId) {
|
||||
return fileService.listByTopic("notice", topicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量绑定附件到通知。
|
||||
*/
|
||||
@PostMapping("fileBind")
|
||||
public void bindFiles(@RequestParam("fileIds") List<Integer> fileIds,
|
||||
@RequestParam("topicId") Integer topicId) {
|
||||
@PostMapping("Bind")
|
||||
public void bind(@RequestParam("fileIds") List<Integer> fileIds,
|
||||
@RequestParam("topicId") Integer topicId) {
|
||||
fileService.bindToTopic(fileIds, topicId);
|
||||
}
|
||||
|
||||
private Map<String, Object> upFileToResult(UpFileVo vo) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("fileid", vo.getId());
|
||||
result.put("name", vo.getFileTrueName());
|
||||
result.put("fileurl", vo.getFileName());
|
||||
result.put("host", "");
|
||||
result.put("type", vo.getFileType());
|
||||
result.put("size", vo.getFileSize());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.common.context.NoticeContext;
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||
import com.yunhai.notice.enums.NoticeTypeEnum;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.*;
|
||||
import com.yunhai.notice.repository.NoticeReadRepository;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import com.yunhai.notice.utils.PageUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通知公告前端对接接口。
|
||||
* <p>匹配前端 Notice3.0 的 URL 路径和参数格式。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 分页接口返回 { list, total, pageIndex, pageSize } 结构到 data 字段。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/notice/")
|
||||
public class NoticeApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoticeApiController.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
@Autowired
|
||||
private NoticeReadRepository noticeReadRepository;
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
// ============================ 已收通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已收通知列表(用户端)。
|
||||
* 前端: GET notice/Notice_list/List
|
||||
*/
|
||||
@GetMapping("Notice_list/List")
|
||||
public Map<String, Object> list(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime,
|
||||
@RequestParam(value = "Name", required = false) String name,
|
||||
@RequestParam(value = "Depttype", required = false) String depttype,
|
||||
@RequestParam(value = "OrderByTime", required = false, defaultValue = "1") String orderByTime,
|
||||
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize,
|
||||
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex) {
|
||||
|
||||
NoticeQuery query = buildQuery(name, depttype, starttime, endtime, orderByTime, pageSize, pageIndex);
|
||||
Sort sort = buildSort(orderByTime);
|
||||
NoticeUserListResult result = noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
|
||||
List<Map<String, Object>> list = result.getContent().stream().map(vo -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("serid", vo.getId());
|
||||
item.put("title", vo.getTitle());
|
||||
item.put("DeptName", vo.getDeptName());
|
||||
item.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
item.put("status", vo.isRead() ? 2 : 1);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("list", list);
|
||||
data.put("total", result.getTotal());
|
||||
data.put("unreadCount", result.getUnreadCount());
|
||||
data.put("pageIndex", pageIndex);
|
||||
data.put("pageSize", pageSize);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================ 已发通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已发/全部通知列表(管理端)。
|
||||
* 前端: GET notice/Notice_list/SendList
|
||||
*/
|
||||
@GetMapping("Notice_list/SendList")
|
||||
public Map<String, Object> sendList(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime,
|
||||
@RequestParam(value = "Name", required = false) String name,
|
||||
@RequestParam(value = "Depttype", required = false) String depttype,
|
||||
@RequestParam(value = "OrderByTime", required = false, defaultValue = "1") String orderByTime,
|
||||
@RequestParam(value = "PageSize", defaultValue = "10") int pageSize,
|
||||
@RequestParam(value = "PageIndex", defaultValue = "1") int pageIndex,
|
||||
@RequestParam(value = "type", required = false) String type) {
|
||||
|
||||
NoticeQuery query = buildQuery(name, depttype, starttime, endtime, orderByTime, pageSize, pageIndex);
|
||||
if ("user".equals(type)) {
|
||||
query.setPublisherAccount(NoticeContext.getAccount());
|
||||
}
|
||||
Sort sort = buildSort(orderByTime);
|
||||
BasePageResult<NoticeVo> result = noticeService.adminList(query, PageUtils.toPageable(query, sort));
|
||||
|
||||
List<Map<String, Object>> list = result.getContent().stream().map(vo -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("serid", vo.getId());
|
||||
item.put("title", vo.getTitle());
|
||||
item.put("DeptName", vo.getDeptName());
|
||||
item.put("TeaName", vo.getPublisherName());
|
||||
item.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
item.put("readcount", vo.getReadCount());
|
||||
item.put("allowcount", vo.getAllowCount() != null ? vo.getAllowCount() : 0);
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", vo.getId());
|
||||
String upFileStr = files.stream()
|
||||
.map(f -> String.valueOf(f.getId()))
|
||||
.collect(Collectors.joining(","));
|
||||
item.put("UpFile", upFileStr.isEmpty() ? null : upFileStr);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("list", list);
|
||||
data.put("total", result.getTotal());
|
||||
data.put("pageIndex", pageIndex);
|
||||
data.put("pageSize", pageSize);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================ 查看/编辑通知详情 ============================
|
||||
|
||||
/**
|
||||
* 查看通知详情(type=0 编辑模式不标记已读, type=1 普通查看标记已读)。
|
||||
* 前端: GET notice/Notice_list/Modify
|
||||
*/
|
||||
@GetMapping("Notice_list/Modify")
|
||||
public Map<String, Object> modify(
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||
NoticeVo vo;
|
||||
if (type == 0) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
vo = new NoticeVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setTypeDesc(NoticeTypeEnum.of(entity.getType()).getDesc());
|
||||
vo.setStatusDesc(NoticeStatusEnum.of(entity.getStatus()).getDesc());
|
||||
vo.setReadCount(noticeReadRepository.countByNoticeId(entity.getId()));
|
||||
} else {
|
||||
vo = noticeService.detail(serid);
|
||||
}
|
||||
return voToDetailMap(vo, serid);
|
||||
}
|
||||
|
||||
// ============================ 获取通知详情(编辑用) ============================
|
||||
|
||||
/**
|
||||
* 获取通知详情(编辑/转发用,不标记已读)。
|
||||
* 前端: GET notice/Notice_list/GetNoticeDetails
|
||||
*/
|
||||
@GetMapping("Notice_list/GetNoticeDetails")
|
||||
public Map<String, Object> getNoticeDetails(@RequestParam("serid") Integer serid) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
NoticeVo vo = new NoticeVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setTypeDesc(NoticeTypeEnum.of(entity.getType()).getDesc());
|
||||
vo.setStatusDesc(NoticeStatusEnum.of(entity.getStatus()).getDesc());
|
||||
vo.setReadCount(noticeReadRepository.countByNoticeId(entity.getId()));
|
||||
return voToDetailMap(vo, serid);
|
||||
}
|
||||
|
||||
// ============================ 新建通知 ============================
|
||||
|
||||
/**
|
||||
* 新建通知。
|
||||
* 前端: POST notice/Notice_list/Create (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("Notice_list/Create")
|
||||
public void create(
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "ispull", required = false, defaultValue = "0") Integer ispull,
|
||||
@RequestParam("Contents") String contents,
|
||||
@RequestParam("ReceiveList") String receiveList,
|
||||
@RequestParam("DeptID") String deptId,
|
||||
@RequestParam(value = "ChaoSend", required = false) String chaoSend,
|
||||
@RequestParam(value = "topmost", required = false, defaultValue = "0") Integer topmost,
|
||||
@RequestParam(value = "DsfsTime", required = false) String dsfsTime) {
|
||||
|
||||
NoticeRequest request = buildNoticeRequest(title, contents, receiveList, deptId, chaoSend, topmost, dsfsTime);
|
||||
noticeService.publish(request);
|
||||
|
||||
if (StringUtils.hasText(chaoSend)) {
|
||||
List<Integer> fileIds = parseIds(chaoSend);
|
||||
Page<Notice> latest = noticeRepository.findAll(
|
||||
PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createTime")));
|
||||
if (latest.hasContent()) {
|
||||
fileService.bindToTopic(fileIds, latest.getContent().get(0).getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 编辑通知 ============================
|
||||
|
||||
/**
|
||||
* 编辑通知保存。
|
||||
* 前端: POST notice/Notice_list/EditNotice (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("Notice_list/EditNotice")
|
||||
public void editNotice(
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam(value = "ispull", required = false, defaultValue = "0") Integer ispull,
|
||||
@RequestParam("Contents") String contents,
|
||||
@RequestParam("ReceiveList") String receiveList,
|
||||
@RequestParam("DeptID") String deptId,
|
||||
@RequestParam(value = "ChaoSend", required = false) String chaoSend,
|
||||
@RequestParam(value = "topmost", required = false, defaultValue = "0") Integer topmost,
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "DsfsTime", required = false) String dsfsTime) {
|
||||
|
||||
NoticeRequest request = buildNoticeRequest(title, contents, receiveList, deptId, chaoSend, topmost, dsfsTime);
|
||||
noticeService.edit(serid, request);
|
||||
|
||||
if (StringUtils.hasText(chaoSend)) {
|
||||
List<Integer> fileIds = parseIds(chaoSend);
|
||||
fileService.bindToTopic(fileIds, serid);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 删除通知 ============================
|
||||
|
||||
/**
|
||||
* 删除通知(支持批量)。
|
||||
* 前端: POST notice/Notice_list/DeleteNotice (form-urlencoded: ids=1,2,3)
|
||||
*/
|
||||
@PostMapping("Notice_list/DeleteNotice")
|
||||
public void deleteNotice(@RequestParam("ids") String ids) {
|
||||
for (String idStr : ids.split(",")) {
|
||||
String trimmed = idStr.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
noticeService.delete(Integer.parseInt(trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 阅读情况 ============================
|
||||
|
||||
/**
|
||||
* 按组分类查看阅读情况。
|
||||
* 前端: GET notice/Notice_list/OrgizeModify
|
||||
*/
|
||||
@GetMapping("Notice_list/OrgizeModify")
|
||||
public List<Map<String, Object>> orgizeModify(@RequestParam("serid") Integer serid) {
|
||||
List<NoticeVo.ReadDetail> readDetails = noticeService.getReadDetail(serid);
|
||||
List<Map<String, Object>> teaslist = readDetails.stream().map(d -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("teaname", d.getName());
|
||||
item.put("states", "read");
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> group = new LinkedHashMap<>();
|
||||
group.put("GroupName", "已读人员");
|
||||
group.put("teaslist", teaslist);
|
||||
return Collections.singletonList(group);
|
||||
}
|
||||
|
||||
// ============================ 追加接收人 ============================
|
||||
|
||||
/**
|
||||
* 追加接收人。
|
||||
* 前端: POST notice/Notice_list/ReceiveUsers
|
||||
*/
|
||||
@PostMapping("Notice_list/ReceiveUsers")
|
||||
public void receiveUsers(
|
||||
@RequestParam("serid") Integer serid,
|
||||
@RequestParam("users") Integer[] users) {
|
||||
Notice entity = noticeRepository.findById(serid)
|
||||
.orElseThrow(() -> new NoticeException("通知不存在: " + serid));
|
||||
int addCount = users != null ? users.length : 0;
|
||||
entity.setAllowCount((entity.getAllowCount() == null ? 0 : entity.getAllowCount()) + addCount);
|
||||
noticeRepository.save(entity);
|
||||
}
|
||||
|
||||
// ============================ 一键全部已读 ============================
|
||||
|
||||
/**
|
||||
* 一键全部已读。
|
||||
* 前端: GET notice/Notice_list/InitializeRed
|
||||
*/
|
||||
@GetMapping("Notice_list/InitializeRed")
|
||||
public void initializeRed() {
|
||||
String account = NoticeContext.getAccount();
|
||||
List<Integer> readIds = noticeReadRepository.findNoticeIdsByReaderAccount(account);
|
||||
Set<Integer> readSet = new HashSet<>(readIds);
|
||||
List<Notice> published = noticeRepository.findAll((root, query, cb) ->
|
||||
cb.equal(root.get("status"), NoticeStatusEnum.PUBLISHED.getCode()));
|
||||
for (Notice n : published) {
|
||||
if (!readSet.contains(n.getId())) {
|
||||
noticeService.markRead(n.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 批量下载附件 ============================
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件。
|
||||
* 前端: GET notice/Notice_list/AllFileDown?dataid=xxx
|
||||
*/
|
||||
@GetMapping("Notice_list/AllFileDown")
|
||||
public void allFileDown(@RequestParam("dataid") Integer dataid,
|
||||
javax.servlet.http.HttpServletResponse response) {
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", dataid);
|
||||
if (files.isEmpty()) {
|
||||
throw new NoticeException("无附件可下载");
|
||||
}
|
||||
if (files.size() == 1) {
|
||||
fileService.download(files.get(0).getId(), response);
|
||||
} else {
|
||||
fileService.download(files.get(0).getId(), response);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 通知统计 ============================
|
||||
|
||||
/**
|
||||
* 通知统计。
|
||||
* 前端: GET notice/Notice_statis/List
|
||||
*/
|
||||
@GetMapping("Notice_statis/List")
|
||||
public NoticeStatisticsVo statisData(
|
||||
@RequestParam(value = "starttime", required = false) String starttime,
|
||||
@RequestParam(value = "endtime", required = false) String endtime) {
|
||||
LocalDateTime start = StringUtils.hasText(starttime)
|
||||
? LocalDateTime.parse(starttime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
|
||||
LocalDateTime end = StringUtils.hasText(endtime)
|
||||
? LocalDateTime.parse(endtime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
|
||||
return noticeService.statistics(new NoticeQuery(), start, end);
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
private NoticeQuery buildQuery(String name, String depttype, String starttime, String endtime,
|
||||
String orderByTime, int pageSize, int pageIndex) {
|
||||
NoticeQuery query = new NoticeQuery();
|
||||
query.setKeyword(name);
|
||||
query.setDeptId(depttype);
|
||||
query.setPageSize(pageSize);
|
||||
query.setPageIndex(pageIndex);
|
||||
query.setOrderByTime(orderByTime);
|
||||
if (StringUtils.hasText(starttime)) {
|
||||
query.setStartTime(LocalDate.parse(starttime).atStartOfDay());
|
||||
}
|
||||
if (StringUtils.hasText(endtime)) {
|
||||
query.setEndTime(LocalDate.parse(endtime).atTime(23, 59, 59));
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private Sort buildSort(String orderByTime) {
|
||||
Sort sort = "2".equals(orderByTime)
|
||||
? Sort.by(Sort.Direction.ASC, "createTime")
|
||||
: Sort.by(Sort.Direction.DESC, "createTime");
|
||||
return sort.and(Sort.by(Sort.Direction.DESC, "topmost"));
|
||||
}
|
||||
|
||||
private NoticeRequest buildNoticeRequest(String title, String contents, String receiveList,
|
||||
String deptId, String chaoSend, Integer topmost, String dsfsTime) {
|
||||
NoticeRequest request = new NoticeRequest();
|
||||
request.setTitle(title);
|
||||
request.setContent(contents);
|
||||
request.setReceiveList(receiveList);
|
||||
request.setDeptId(deptId);
|
||||
request.setChaoSend(chaoSend);
|
||||
request.setTopmost(topmost);
|
||||
request.setSmsFlag(topmost);
|
||||
if (StringUtils.hasText(dsfsTime)) {
|
||||
request.setTimingSendTime(LocalDateTime.parse(dsfsTime.replace("/", "-"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private Map<String, Object> voToDetailMap(NoticeVo vo, Integer serid) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("title", vo.getTitle());
|
||||
data.put("Contents", vo.getContent());
|
||||
data.put("DeptName", vo.getDeptName());
|
||||
data.put("DeptID", vo.getDeptId());
|
||||
data.put("TeaName", vo.getPublisherName());
|
||||
data.put("EditDate", formatTime(vo.getCreateTime()));
|
||||
data.put("numcount", vo.getAllowCount() != null ? vo.getAllowCount() : 0);
|
||||
data.put("readnumcount", vo.getReadCount());
|
||||
data.put("ReceiveList", vo.getReceiveList());
|
||||
data.put("DsfsTime", vo.getTimingSendTime() != null ? formatTime(vo.getTimingSendTime()) : null);
|
||||
data.put("isDsfs", vo.getTimingSendTime() != null);
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", serid);
|
||||
List<Map<String, Object>> chaoSend = files.stream().map(f -> {
|
||||
Map<String, Object> fileMap = new LinkedHashMap<>();
|
||||
fileMap.put("id", f.getId());
|
||||
fileMap.put("name", f.getFileTrueName());
|
||||
fileMap.put("size", f.getFileSize());
|
||||
fileMap.put("fileid", f.getId());
|
||||
fileMap.put("filename", f.getFileName());
|
||||
return fileMap;
|
||||
}).collect(Collectors.toList());
|
||||
data.put("ChaoSend", chaoSend);
|
||||
data.put("tlist", "all");
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<Integer> parseIds(String idsStr) {
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
for (String s : idsStr.split(",")) {
|
||||
String trimmed = s.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
try {
|
||||
ids.add(Integer.parseInt(trimmed));
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[parseIds] 无法解析文件ID: {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private String formatTime(LocalDateTime time) {
|
||||
if (time == null) return null;
|
||||
return time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,217 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.common.validation.ValidationGroups;
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import com.yunhai.notice.model.notice.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.model.notice.*;
|
||||
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.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知公告接口。
|
||||
* <p>路径前缀:/{platform}/base/(platform = api / manage / mobiapi),
|
||||
* 与 UserBase BaseController 路径习惯一致,前端无需区分子应用 。</p>
|
||||
* 通知公告接口(合并 NoticeApiController + 旧 NoticeController)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/notice/"}(platform = api / manage / mobiapi)。
|
||||
* 方法动词式 PascalCase(对齐 UserBase);写操作一律 POST。
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = Config.API_PATH + "/base/")
|
||||
@RequestMapping(value = Config.API_PATH + "/notice/")
|
||||
public class NoticeController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NoticeController.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Autowired
|
||||
private OpenApiUserClient openApiUserClient;
|
||||
// ============================ 收件箱列表 ============================
|
||||
|
||||
@GetMapping("GetUserInfo")
|
||||
public BoaUser getUserInfo(BoaUser authUserWithDept) {
|
||||
return authUserWithDept;
|
||||
}
|
||||
|
||||
@GetMapping("GetUserMod")
|
||||
public List<BoaModule> getUserMod(BoaUser authUserWithDept) {
|
||||
Integer schoolId = (authUserWithDept.getSchool() != null) ? authUserWithDept.getSchool().getId() : null;
|
||||
List<BoaModule> modules = YschBaseOpenApi.getUserModule(authUserWithDept.getId(), schoolId);
|
||||
return modules;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("GetTree")
|
||||
public void GetTree(BoaUser authUserWithDept) {
|
||||
YschBaseOpenApi.deleteUser(10022,6,27);
|
||||
}
|
||||
|
||||
/** 发通知 */
|
||||
@PostMapping("publish")
|
||||
public void publish(@RequestBody NoticeRequest request) {
|
||||
noticeService.publish(request);
|
||||
}
|
||||
|
||||
/** 保存草稿 */
|
||||
@PostMapping("saveDraft")
|
||||
public Integer saveDraft(@RequestBody NoticeRequest request) {
|
||||
return noticeService.saveDraft(request);
|
||||
}
|
||||
|
||||
/** 编辑通知 */
|
||||
@PostMapping("edit/{id}")
|
||||
public void edit(@PathVariable Integer id, @RequestBody NoticeRequest request) {
|
||||
noticeService.edit(id, request);
|
||||
}
|
||||
|
||||
/** 删除通知 */
|
||||
@PostMapping("delete/{id}")
|
||||
public void delete(@PathVariable Integer id) {
|
||||
noticeService.delete(id);
|
||||
}
|
||||
|
||||
/** 撤回通知 */
|
||||
@PostMapping("pull/{id}")
|
||||
public void pull(@PathVariable Integer id) {
|
||||
noticeService.pull(id);
|
||||
}
|
||||
|
||||
/** 置顶/取消置顶 */
|
||||
@PostMapping("setTopmost/{id}/{topmost}")
|
||||
public void setTopmost(@PathVariable Integer id, @PathVariable Integer topmost) {
|
||||
noticeService.setTopmost(id, topmost);
|
||||
}
|
||||
|
||||
/** 转发通知(基于源通知创建草稿) */
|
||||
@PostMapping("forward/{sourceId}")
|
||||
public Integer forward(@PathVariable Integer sourceId, @RequestBody NoticeRequest request) {
|
||||
return noticeService.forward(sourceId, request);
|
||||
}
|
||||
|
||||
/** 阅读明细列表 */
|
||||
@GetMapping("readDetail/{noticeId}")
|
||||
public List<NoticeVo.ReadDetail> readDetail(@PathVariable Integer noticeId) {
|
||||
return noticeService.getReadDetail(noticeId);
|
||||
}
|
||||
|
||||
/** 用户端列表:仅已发布通知,标记当前用户是否已读,附未读总数(列表有新发的通知) */
|
||||
@GetMapping("userList")
|
||||
public NoticeUserListResult userList(NoticeQuery query,
|
||||
@SortDefault(sort = {"topmost", "createTime"},
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
/**
|
||||
* 已收通知列表(用户端)。
|
||||
*/
|
||||
@GetMapping("List")
|
||||
public NoticePageResult<NoticeVo> list(NoticeQuery query,
|
||||
@SortDefault(sort = {"topmost", "createTime"},
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.userList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
/** 管理端列表:管理员查看全部通知 */
|
||||
@GetMapping("adminList")
|
||||
public BasePageResult<NoticeVo> adminList(NoticeQuery query,
|
||||
@SortDefault(sort = "createTime",
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
// ============================ 已发/全部通知列表 ============================
|
||||
|
||||
/**
|
||||
* 已发/全部通知列表(管理端)。type=user 只看自己。
|
||||
*/
|
||||
@GetMapping("SendList")
|
||||
public BasePageResult<NoticeVo> sendList(NoticeQuery query,
|
||||
@SortDefault(sort = "createTime",
|
||||
direction = Sort.Direction.DESC) Sort sort) {
|
||||
return noticeService.adminList(query, PageUtils.toPageable(query, sort));
|
||||
}
|
||||
|
||||
/** 通知详情:返回详情并顺带标记当前用户已读 */
|
||||
@GetMapping("detail/{id}")
|
||||
public NoticeVo detail(@PathVariable Integer id) {
|
||||
return noticeService.detail(id);
|
||||
// ============================ 查看详情(标读) ============================
|
||||
|
||||
/**
|
||||
* 查看通知详情(type=1 普通查看标记已读, type=0 编辑模式不标记已读)。
|
||||
*/
|
||||
@GetMapping("Detail")
|
||||
public NoticeDetailVo detail(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "type", defaultValue = "1") Integer type) {
|
||||
return noticeService.detailMap(serid, type);
|
||||
}
|
||||
|
||||
/** 标记已读(前端主动调用,幂等) */
|
||||
@PostMapping("read/{id}")
|
||||
public void read(@PathVariable Integer id) {
|
||||
noticeService.markRead(id);
|
||||
// ============================ 编辑回显(不标读) ============================
|
||||
|
||||
/**
|
||||
* 获取通知详情(编辑/转发用,不标记已读)。
|
||||
*/
|
||||
@GetMapping("GetDetail")
|
||||
public NoticeDetailVo getDetail(@RequestParam("serid") Integer serid) {
|
||||
return noticeService.getDetailMap(serid);
|
||||
}
|
||||
|
||||
/** 通知统计:总数/已发布/草稿、累计阅读人次、当前用户未读、按发布人、按月 */
|
||||
@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) {
|
||||
// ============================ 新建 ============================
|
||||
|
||||
/**
|
||||
* 新建通知(JSON body)。fileIds 为附件ID逗号串,smsFlag 为短信标记。
|
||||
*/
|
||||
@PostMapping("Create")
|
||||
public Integer create(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) {
|
||||
return noticeService.publish(request);
|
||||
}
|
||||
|
||||
// ============================ 编辑 ============================
|
||||
|
||||
/**
|
||||
* 编辑通知(JSON body,含 id)。
|
||||
*/
|
||||
@PostMapping("Edit")
|
||||
public void edit(@RequestBody @Validated(ValidationGroups.Update.class) NoticeRequest request) {
|
||||
noticeService.edit(request.getSourceId() != null ? request.getSourceId() : 0, request);
|
||||
}
|
||||
|
||||
// ============================ 批量删除 ============================
|
||||
|
||||
/**
|
||||
* 批量删除通知(ids 数组)。
|
||||
*/
|
||||
@PostMapping("Delete")
|
||||
public void delete(@RequestBody List<Integer> ids) {
|
||||
noticeService.deleteBatch(ids);
|
||||
}
|
||||
|
||||
// ============================ 撤回 ============================
|
||||
|
||||
/**
|
||||
* 撤回通知。
|
||||
*/
|
||||
@PostMapping("UserPull")
|
||||
public void userPull(@RequestParam("serid") Integer serid) {
|
||||
noticeService.pull(serid);
|
||||
}
|
||||
|
||||
// ============================ 置顶 ============================
|
||||
|
||||
/**
|
||||
* 置顶/取消置顶。
|
||||
*/
|
||||
@PostMapping("SetTopmost")
|
||||
public void setTopmost(@RequestParam("serid") Integer serid,
|
||||
@RequestParam(value = "topmost", defaultValue = "1") Integer topmost) {
|
||||
noticeService.setTopmost(serid, topmost);
|
||||
}
|
||||
|
||||
// ============================ 追加接收人 ============================
|
||||
|
||||
/**
|
||||
* 追加接收人。
|
||||
*/
|
||||
@PostMapping("ReceiveUsers")
|
||||
public void receiveUsers(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("users") String users) {
|
||||
noticeService.receiveUsers(serid, users);
|
||||
}
|
||||
|
||||
// ============================ 催收 ============================
|
||||
|
||||
/**
|
||||
* 催收未读。
|
||||
*/
|
||||
@PostMapping("Expedite")
|
||||
public void expedite(@RequestParam("serid") Integer serid) {
|
||||
noticeService.expedite(serid);
|
||||
}
|
||||
|
||||
// ============================ 一键已读 ============================
|
||||
|
||||
/**
|
||||
* 一键全部已读(POST 化,原为 GET 写操作)。
|
||||
*/
|
||||
@PostMapping("InitializeRed")
|
||||
public InitializeRedVo initializeRed() {
|
||||
return noticeService.initializeRedResult();
|
||||
}
|
||||
|
||||
// ============================ 阅读情况 ============================
|
||||
|
||||
/**
|
||||
* 阅读情况(按已读/未读分组)。
|
||||
*/
|
||||
@GetMapping("ReadDetail")
|
||||
public List<Map<String, Object>> readDetail(@RequestParam("serid") Integer serid) {
|
||||
return noticeService.getReadDetailGrouped(serid);
|
||||
}
|
||||
|
||||
// ============================ 上一篇/下一篇 ============================
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇通知ID。
|
||||
*/
|
||||
@GetMapping("GetNextPrev")
|
||||
public Map<String, Object> getNextPrev(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("type") String type) {
|
||||
return noticeService.getNextPrev(serid, type);
|
||||
}
|
||||
|
||||
// ============================ 附件打包下载 ============================
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件(打包 zip)。
|
||||
*/
|
||||
@GetMapping("AllFileDown")
|
||||
public void allFileDown(@RequestParam("dataid") Integer dataid,
|
||||
HttpServletResponse response) {
|
||||
noticeService.downloadAllFiles(dataid, response);
|
||||
}
|
||||
|
||||
// ============================ 统计 ============================
|
||||
|
||||
/**
|
||||
* 通知统计。
|
||||
*/
|
||||
@GetMapping("Statis")
|
||||
public NoticeStatisticsVo statis(
|
||||
@RequestParam(value = "starttime", required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
|
||||
@RequestParam(value = "endtime", required = false)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) {
|
||||
return noticeService.statistics(new NoticeQuery(), start, end);
|
||||
}
|
||||
|
||||
// ============================ 转发(合并旧 REST 壳 forward) ============================
|
||||
|
||||
/**
|
||||
* 转发通知(基于源通知创建新通知)。sourceId 在 request body 中传递。
|
||||
*/
|
||||
@PostMapping("Forward")
|
||||
public Integer forward(@RequestBody @Validated(ValidationGroups.Create.class) NoticeRequest request) {
|
||||
return noticeService.forward(request.getSourceId(), request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +1,88 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.SmsService;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.service.ISmsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信设置接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/schsms/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 短信设置接口(短信域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。
|
||||
* 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/schsms/")
|
||||
@RequestMapping(value = Config.API_PATH + "/sms/")
|
||||
public class SmsController {
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
// ============================ getmodule ============================
|
||||
private ISmsService smsService;
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
* 前端: GET schsms/getmodule
|
||||
*/
|
||||
@GetMapping("getmodule")
|
||||
public List<Map<String, Object>> getModuleList() {
|
||||
@GetMapping("GetModule")
|
||||
public List<Map<String, Object>> getModule() {
|
||||
return smsService.getModuleList();
|
||||
}
|
||||
|
||||
// ============================ getgroup ============================
|
||||
|
||||
/**
|
||||
* 获取学校父类组列表。
|
||||
* 前端: GET schsms/getgroup
|
||||
*/
|
||||
@GetMapping("getgroup")
|
||||
public List<Map<String, Object>> getSchParentGroup() {
|
||||
@GetMapping("GetGroup")
|
||||
public List<Map<String, Object>> getGroup() {
|
||||
return smsService.getSchParentGroup();
|
||||
}
|
||||
|
||||
// ============================ getschsms ============================
|
||||
|
||||
/**
|
||||
* 分页获取学校短信开关列表。
|
||||
* 前端: GET schsms/getschsms?PageSize=&PageIndex=&SchName=
|
||||
*/
|
||||
@GetMapping("getschsms")
|
||||
public Map<String, Object> getSchIsSmsList(
|
||||
@GetMapping("GetSchoolSms")
|
||||
public Map<String, Object> getSchoolSms(
|
||||
@RequestParam(value = "schpid", required = false) String schpid,
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "SchName", required = false, defaultValue = "") String schName) {
|
||||
// 前端 PageIndex 从0开始(page-1),后端按0基处理
|
||||
return smsService.getSchIsSmsList(pageSize, pageIndex, schName);
|
||||
}
|
||||
|
||||
// ============================ getsch ============================
|
||||
|
||||
/**
|
||||
* 获取学校列表。
|
||||
* 前端: GET schsms/getsch
|
||||
*/
|
||||
@GetMapping("getsch")
|
||||
public List<Map<String, Object>> getSchList() {
|
||||
@GetMapping("GetSchool")
|
||||
public List<Map<String, Object>> getSchool() {
|
||||
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);
|
||||
@PostMapping("SetSchoolSms")
|
||||
public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) {
|
||||
smsService.setSchSmsFromJson(itemsModuJson);
|
||||
}
|
||||
|
||||
// ============================ setschallsms ============================
|
||||
|
||||
/**
|
||||
* 设置某校全部模块短信开关。
|
||||
* 前端: GET schsms/setschallsms?SchSerID=&IsSms=
|
||||
* 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("setschallsms")
|
||||
public void setSchSmsAllTrue(
|
||||
@PostMapping("SetAllSchoolSms")
|
||||
public void setAllSchoolSms(
|
||||
@RequestParam("SchSerID") Integer schSerID,
|
||||
@RequestParam("IsSms") Boolean isSms) {
|
||||
smsService.setSchSmsAll(schSerID, isSms);
|
||||
}
|
||||
|
||||
// ============================ setschmodulesms ============================
|
||||
|
||||
/**
|
||||
* 批量设置多校多模块短信开关。
|
||||
* 前端: GET schsms/setschmodulesms?SchSerID=1,2&Module=3,4&IsSms=true
|
||||
* 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("setschmodulesms")
|
||||
public void setSchModuleSmsTrue(
|
||||
@PostMapping("SetSchoolModuleSms")
|
||||
public void setSchoolModuleSms(
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +1,67 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.service.TemplateService;
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.service.ITemplateService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知模板接口(兼容前端 Notice3.0 URL)。
|
||||
* 路径前缀: /{platform}/notice/Template/
|
||||
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
|
||||
* 通知模板接口(模板域)。
|
||||
* <p>路径前缀:{@code Config.API_PATH + "/template/"}(原 notice/Template/ 三级路径已修正)。
|
||||
* 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/{platform}/notice/Template/")
|
||||
@RequestMapping(value = Config.API_PATH + "/template/")
|
||||
public class TemplateController {
|
||||
|
||||
@Autowired
|
||||
private TemplateService templateService;
|
||||
|
||||
// ============================ GetTypelist ============================
|
||||
private ITemplateService templateService;
|
||||
|
||||
/**
|
||||
* 获取模板类型列表。
|
||||
* 前端: GET notice/Template/GetTypelist
|
||||
*/
|
||||
@GetMapping("GetTypelist")
|
||||
@GetMapping("Typelist")
|
||||
public List<Map<String, Object>> getTypelist() {
|
||||
return templateService.getTypelist();
|
||||
}
|
||||
|
||||
// ============================ EditType ============================
|
||||
|
||||
/**
|
||||
* 新增/编辑模板类型。
|
||||
* 前端: GET notice/Template/EditType?typeid=&TypeName=
|
||||
* 新增/编辑模板类型(原 GET EditType,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("EditType")
|
||||
public void editType(
|
||||
@PostMapping("TypeEdit")
|
||||
public void typeEdit(
|
||||
@RequestParam("typeid") Integer typeid,
|
||||
@RequestParam("TypeName") String typeName) {
|
||||
templateService.editType(typeid, typeName);
|
||||
}
|
||||
|
||||
// ============================ DelType ============================
|
||||
|
||||
/**
|
||||
* 删除模板类型。
|
||||
* 前端: GET notice/Template/DelType?typeid=
|
||||
* 删除模板类型(原 GET DelType,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("DelType")
|
||||
public void delType(@RequestParam("typeid") Integer typeid) {
|
||||
@PostMapping("TypeDelete")
|
||||
public void typeDelete(@RequestParam("typeid") Integer typeid) {
|
||||
templateService.delType(typeid);
|
||||
}
|
||||
|
||||
// ============================ GetTemplatelist ============================
|
||||
|
||||
/**
|
||||
* 分页获取模板列表。
|
||||
* 前端: GET notice/Template/GetTemplatelist?PageSize=&PageIndex=&typeid=
|
||||
*/
|
||||
@GetMapping("GetTemplatelist")
|
||||
public Map<String, Object> getTemplatelist(
|
||||
@GetMapping("List")
|
||||
public Map<String, Object> list(
|
||||
@RequestParam("PageSize") Integer pageSize,
|
||||
@RequestParam("PageIndex") Integer pageIndex,
|
||||
@RequestParam(value = "typeid", defaultValue = "0") Integer typeid) {
|
||||
return templateService.getTemplatelist(pageSize, pageIndex, typeid);
|
||||
}
|
||||
|
||||
// ============================ EditTemplate ============================
|
||||
|
||||
/**
|
||||
* 新增/编辑模板。
|
||||
* 前端: POST notice/Template/EditTemplate (form-urlencoded)
|
||||
*/
|
||||
@PostMapping("EditTemplate")
|
||||
public void editTemplate(
|
||||
@PostMapping("TemplateEdit")
|
||||
public void templateEdit(
|
||||
@RequestParam(value = "SerID", required = false) Integer serID,
|
||||
@RequestParam("Title") String title,
|
||||
@RequestParam("Contents") String contents,
|
||||
@@ -83,25 +70,19 @@ public class TemplateController {
|
||||
templateService.editTemplate(serID, title, contents, type, cover);
|
||||
}
|
||||
|
||||
// ============================ DelTemplate ============================
|
||||
|
||||
/**
|
||||
* 删除模板。
|
||||
* 前端: GET notice/Template/DelTemplate?serid=
|
||||
* 删除模板(原 GET DelTemplate,已 POST 化)。
|
||||
*/
|
||||
@GetMapping("DelTemplate")
|
||||
public void delTemplate(@RequestParam("SerID") Integer serid) {
|
||||
@PostMapping("TemplateDelete")
|
||||
public void templateDelete(@RequestParam("SerID") Integer serid) {
|
||||
templateService.delTemplate(serid);
|
||||
}
|
||||
|
||||
// ============================ GetTemplateFist ============================
|
||||
|
||||
/**
|
||||
* 获取单个模板详情。
|
||||
* 前端: GET notice/Template/GetTemplateFist?serid=
|
||||
*/
|
||||
@GetMapping("GetTemplateFist")
|
||||
public Map<String, Object> getTemplateFist(@RequestParam("serid") Integer serid) {
|
||||
@GetMapping("TemplateFirst")
|
||||
public Map<String, Object> templateFirst(@RequestParam("serid") Integer serid) {
|
||||
return templateService.getTemplateFist(serid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -27,10 +25,15 @@ public class Notice extends BaseEntity {
|
||||
/**
|
||||
* 通知标题
|
||||
*/
|
||||
@NotBlank(message = "通知标题不得为空")
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。
|
||||
*/
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/**
|
||||
* 通知内容
|
||||
*/
|
||||
@@ -40,7 +43,6 @@ public class Notice extends BaseEntity {
|
||||
/**
|
||||
* 通知类型:1 普通 / 2 重要(见 {@link com.yunhai.notice.enums.NoticeTypeEnum})
|
||||
*/
|
||||
@NotNull
|
||||
@Column(name = "type", nullable = false)
|
||||
private Integer type = 1;
|
||||
|
||||
@@ -69,9 +71,8 @@ public class Notice extends BaseEntity {
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 状态:0 草稿 / 1 已发布(见 {@link com.yunhai.notice.enums.NoticeStatusEnum})
|
||||
* 状态:1 已发布(无草稿态,见 {@link com.yunhai.notice.enums.NoticeStatusEnum})
|
||||
*/
|
||||
@NotNull
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yunhai.notice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* 通知接收人关联表(替代旧 receive_list JSON 字符串存储)。
|
||||
* <p>
|
||||
* 每条记录代表一个通知的一个接收人。全员通知(receive_list="ALL")不写明细行,
|
||||
* 读取时按 receive_list="ALL" 标识走旧逻辑兜底。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_receiver",
|
||||
indexes = {
|
||||
@Index(name = "idx_notice_id", columnList = "notice_id"),
|
||||
@Index(name = "idx_account", columnList = "account")
|
||||
})
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeReceiver extends BaseEntity {
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 接收人登录账号 */
|
||||
@Column(name = "account", nullable = false, length = 64)
|
||||
private String account;
|
||||
|
||||
/** 接收类型:1 接收 / 2 抄送 */
|
||||
@Column(name = "receive_type", nullable = false)
|
||||
private Integer receiveType = 1;
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package com.yunhai.notice.enums;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通知状态。对应原 .NET Notify_Message.status(原系统已发布=2,此处归一化)。
|
||||
* 通知状态。无草稿态:发通知一律置为「已发布」,
|
||||
* 定时发送通过 timingSendTime + 查询侧「已到点」过滤控制可见性。
|
||||
* 对应原 .NET Notify_Message.status(原系统已发布=2,此处归一化为 1)。
|
||||
*/
|
||||
@Getter
|
||||
public enum NoticeStatusEnum {
|
||||
|
||||
DRAFT(0, "草稿"),
|
||||
PUBLISHED(1, "已发布");
|
||||
|
||||
private final int code;
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.yunhai.notice.job;
|
||||
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.enums.NoticeStatusEnum;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.service.INoticeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时发送任务:每分钟扫描草稿状态且 timingSendTime <= now 的通知,自动发布。
|
||||
* <p>
|
||||
* 多实例部署时需加分布式锁(ShedLock 或 Quartz 集群),避免重复执行。
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "timing-send.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class TimingSendJob {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TimingSendJob.class);
|
||||
|
||||
@Autowired
|
||||
private NoticeRepository noticeRepository;
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Value("${timing-send.cron:0 * * * * ?}")
|
||||
private String cron;
|
||||
|
||||
/**
|
||||
* 每分钟执行一次,扫描到期的定时通知并发送。
|
||||
*/
|
||||
@Scheduled(cron = "${timing-send.cron:0 * * * * ?}")
|
||||
public void executeTimingSend() {
|
||||
List<Notice> pendingList = noticeRepository.findPendingTimingSend(
|
||||
NoticeStatusEnum.DRAFT.getCode(), LocalDateTime.now());
|
||||
if (pendingList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("[TimingSendJob] 发现 {} 条待发送通知", pendingList.size());
|
||||
for (Notice notice : pendingList) {
|
||||
try {
|
||||
noticeService.sendNotify(notice.getId());
|
||||
log.info("[TimingSendJob] 通知 {} 定时发送成功", notice.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("[TimingSendJob] 通知 {} 定时发送失败", notice.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.yunhai.notice.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。
|
||||
* 对应 C# BestSchool.Entity.Model.SchTeaList。
|
||||
*/
|
||||
public class SchTeaList {
|
||||
|
||||
/** 学校 GUID(SchSerID) */
|
||||
@JsonProperty("SchSerID")
|
||||
private String schSerID;
|
||||
|
||||
@JsonProperty("TeaTel")
|
||||
private String teaTel;
|
||||
|
||||
/** 教师姓名 */
|
||||
@JsonProperty("TeaName")
|
||||
private String teaName;
|
||||
|
||||
/** 教师用户ID */
|
||||
@JsonProperty("UserTeaSerID")
|
||||
private Integer userTeaSerID;
|
||||
|
||||
@JsonProperty("TeaIDCard")
|
||||
private String teaIDCard;
|
||||
|
||||
/** 教师工号 */
|
||||
@JsonProperty("TeaNo")
|
||||
private String teaNo;
|
||||
|
||||
/** 教师所属组ID列表(兼容旧字段 GroupIds) */
|
||||
@JsonProperty("GroupIds")
|
||||
private List<Integer> groupIds;
|
||||
|
||||
/** 教师所属组ID(接口现行字段 GroupID,单个) */
|
||||
@JsonProperty("GroupID")
|
||||
private Integer groupId;
|
||||
|
||||
public Integer getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Integer groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getSchSerID() {
|
||||
return schSerID;
|
||||
}
|
||||
|
||||
public void setSchSerID(String schSerID) {
|
||||
this.schSerID = schSerID;
|
||||
}
|
||||
|
||||
public String getTeaTel() {
|
||||
return teaTel;
|
||||
}
|
||||
|
||||
public void setTeaTel(String teaTel) {
|
||||
this.teaTel = teaTel;
|
||||
}
|
||||
|
||||
public String getTeaName() {
|
||||
return teaName;
|
||||
}
|
||||
|
||||
public void setTeaName(String teaName) {
|
||||
this.teaName = teaName;
|
||||
}
|
||||
|
||||
public Integer getUserTeaSerID() {
|
||||
return userTeaSerID;
|
||||
}
|
||||
|
||||
public void setUserTeaSerID(Integer userTeaSerID) {
|
||||
this.userTeaSerID = userTeaSerID;
|
||||
}
|
||||
|
||||
public String getTeaIDCard() {
|
||||
return teaIDCard;
|
||||
}
|
||||
|
||||
public void setTeaIDCard(String teaIDCard) {
|
||||
this.teaIDCard = teaIDCard;
|
||||
}
|
||||
|
||||
public String getTeaNo() {
|
||||
return teaNo;
|
||||
}
|
||||
|
||||
public void setTeaNo(String teaNo) {
|
||||
this.teaNo = teaNo;
|
||||
}
|
||||
|
||||
public List<Integer> getGroupIds() {
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public void setGroupIds(List<Integer> groupIds) {
|
||||
this.groupIds = groupIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 一键已读结果视图对象(替代原 Map<String,Object> 返回)。
|
||||
*/
|
||||
@Data
|
||||
public class InitializeRedVo {
|
||||
|
||||
@JsonProperty("count")
|
||||
private Integer count;
|
||||
|
||||
public InitializeRedVo() {}
|
||||
|
||||
public InitializeRedVo(Integer count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知详情视图对象(替代原 Map<String,Object> 返回)。
|
||||
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
|
||||
*/
|
||||
@Data
|
||||
public class NoticeDetailVo {
|
||||
|
||||
@JsonProperty("serid")
|
||||
private Integer serid;
|
||||
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@JsonProperty("Contents")
|
||||
private String contents;
|
||||
|
||||
@JsonProperty("DeptID")
|
||||
private String deptId;
|
||||
|
||||
@JsonProperty("DeptName")
|
||||
private String deptName;
|
||||
|
||||
@JsonProperty("TeaName")
|
||||
private String publisherName;
|
||||
|
||||
@JsonProperty("EditDate")
|
||||
private String editDate;
|
||||
|
||||
@JsonProperty("numcount")
|
||||
private Integer numCount;
|
||||
|
||||
@JsonProperty("readnumcount")
|
||||
private Long readNumCount;
|
||||
|
||||
@JsonProperty("ReceiveList")
|
||||
private String receiveList;
|
||||
|
||||
@JsonProperty("DsfsTime")
|
||||
private String dsfsTime;
|
||||
|
||||
@JsonProperty("isDsfs")
|
||||
private Boolean isDsfs;
|
||||
|
||||
@JsonProperty("ChaoSend")
|
||||
private List<FileItem> chaoSend;
|
||||
|
||||
/** 接收人列表:"all" 或账号列表 */
|
||||
@JsonProperty("tlist")
|
||||
private Object tlist;
|
||||
|
||||
/**
|
||||
* 附件项(旧 ChaoSend 数组元素)。
|
||||
*/
|
||||
@Data
|
||||
public static class FileItem {
|
||||
@JsonProperty("id")
|
||||
private Integer id;
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
@JsonProperty("size")
|
||||
private Integer size;
|
||||
@JsonProperty("fileid")
|
||||
private Integer fileid;
|
||||
@JsonProperty("filename")
|
||||
private String filename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import com.yunhai.notice.model.BasePageResult;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知分页结果(扩展 BasePageResult,额外携带 unreadCount)。
|
||||
* <p>
|
||||
* 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class NoticePageResult<T> extends BasePageResult<T> {
|
||||
|
||||
/** 当前用户未读通知总数 */
|
||||
private long unreadCount;
|
||||
|
||||
public NoticePageResult(List<T> content, int total, int pageIndex, int pageSize,
|
||||
int totalPages, boolean hasNext, long unreadCount) {
|
||||
super(content, total, pageIndex, pageSize, totalPages, hasNext);
|
||||
this.unreadCount = unreadCount;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 通知列表查询条件。
|
||||
@@ -17,25 +17,28 @@ import java.time.LocalDateTime;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class NoticeQuery extends BasePageQuery {
|
||||
|
||||
/** 状态过滤:null 不过滤;0 草稿 / 1 已发布 */
|
||||
/** 状态过滤:null 不过滤;1 已发布 */
|
||||
private Integer status;
|
||||
|
||||
/** 类型过滤:1 普通 / 2 重要 */
|
||||
private Integer type;
|
||||
|
||||
/** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */
|
||||
private String scope;
|
||||
|
||||
/** 发布部门ID */
|
||||
private String deptId;
|
||||
|
||||
/** 发布人账号(管理端按发布人查) */
|
||||
private String publisherAccount;
|
||||
|
||||
/** 开始时间(创建时间 >=) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
/** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate startTime;
|
||||
|
||||
/** 结束时间(创建时间 <=) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
/** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate endTime;
|
||||
|
||||
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
|
||||
private Integer readType = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.yunhai.notice.model.notice;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import com.yunhai.notice.common.validation.ValidationGroups;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -14,18 +15,18 @@ import java.time.LocalDateTime;
|
||||
public class NoticeRequest {
|
||||
|
||||
/** 通知标题 */
|
||||
@NotBlank(message = "通知标题不得为空")
|
||||
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知标题不得为空")
|
||||
private String title;
|
||||
|
||||
/** 通知内容 */
|
||||
private String content;
|
||||
|
||||
/** 通知类型:1 普通 / 2 重要 */
|
||||
@NotNull(message = "通知类型不得为空")
|
||||
@NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "通知类型不得为空")
|
||||
private Integer type = 1;
|
||||
|
||||
/** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */
|
||||
@NotBlank(message = "发布部门不得为空")
|
||||
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空")
|
||||
private String deptId;
|
||||
|
||||
/** 发布部门名称 */
|
||||
@@ -34,16 +35,19 @@ public class NoticeRequest {
|
||||
/** 是否置顶:0 否 / 1 是 */
|
||||
private Integer topmost = 0;
|
||||
|
||||
/** 接收人列表(JSON 数组字符串,如 ["account1","account2"]) */
|
||||
/** 接收人列表(JSON 数组字符串,如 ["account1","account2"] 或 "ALL" 表示全员) */
|
||||
private String receiveList;
|
||||
|
||||
/** 抄送人列表 */
|
||||
private String chaoSend;
|
||||
/** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */
|
||||
private String fileIds;
|
||||
|
||||
/** 定时发送时间(为空表示立即发布) */
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timingSendTime;
|
||||
|
||||
/** 短信发送标记:0 不发短信 / 1 发短信 */
|
||||
/** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */
|
||||
private Integer smsFlag = 0;
|
||||
|
||||
/** 转发源通知ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */
|
||||
private Integer sourceId;
|
||||
}
|
||||
|
||||
@@ -10,19 +10,16 @@ import java.util.List;
|
||||
@Data
|
||||
public class NoticeStatisticsVo {
|
||||
|
||||
/** 通知总数(含草稿+已发布) */
|
||||
/** 通知总数(全部已发布) */
|
||||
private long total;
|
||||
|
||||
/** 已发布数 */
|
||||
private long publishedCount;
|
||||
|
||||
/** 草稿数 */
|
||||
private long draftCount;
|
||||
|
||||
/** 阅读记录总数(累计已读人次) */
|
||||
private long totalReadRecords;
|
||||
|
||||
/** 当前用户未读数(已发布 - 当前用户已读) */
|
||||
/** 当前用户未读数(已到点的已发布 - 当前用户已读) */
|
||||
private long unreadForCurrentUser;
|
||||
|
||||
/** 按发布人统计 */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文件上传结果视图对象(替代原 Map<String,Object> 返回)。
|
||||
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
|
||||
*/
|
||||
@Data
|
||||
public class UpFileResultVo {
|
||||
|
||||
@JsonProperty("fileid")
|
||||
private Integer fileid;
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("fileurl")
|
||||
private String fileurl;
|
||||
|
||||
@JsonProperty("host")
|
||||
private String host;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("size")
|
||||
private Integer size;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.yunhai.notice.model.notice;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户可访问模块(左菜单)视图对象。
|
||||
* <p>将基础平台返回的扁平 {@code List<BoaModule>} 转换为前端 layout(参考 UserBase)期望的树结构:
|
||||
* {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。</p>
|
||||
*/
|
||||
public class UserModuleVo {
|
||||
|
||||
/** 模块主键 id(基础平台 module id) */
|
||||
private Integer id;
|
||||
|
||||
/** 菜单高亮匹配用:等价于后端模块 id */
|
||||
private Integer modId;
|
||||
|
||||
/** 模块名称 */
|
||||
private String name;
|
||||
|
||||
/** 父级模块 id(0 表示根节点) */
|
||||
private Integer parentId;
|
||||
|
||||
/** 前端路由路径(由 BoaModule.path 映射而来) */
|
||||
private String page;
|
||||
|
||||
/** 层级(0=根分组) */
|
||||
private Integer level;
|
||||
|
||||
/** 菜单图标 */
|
||||
private String icon;
|
||||
|
||||
/** 排序号 */
|
||||
private Integer orderNumber;
|
||||
|
||||
/** 是否可用 */
|
||||
private Boolean usable;
|
||||
|
||||
/** 子菜单 */
|
||||
private List<UserModuleVo> children = new ArrayList<>();
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getModId() {
|
||||
return modId;
|
||||
}
|
||||
|
||||
public void setModId(Integer modId) {
|
||||
this.modId = modId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(Integer parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public String getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(String page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public Integer getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(Integer level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public Integer getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(Integer orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public Boolean getUsable() {
|
||||
return usable;
|
||||
}
|
||||
|
||||
public void setUsable(Boolean usable) {
|
||||
this.usable = usable;
|
||||
}
|
||||
|
||||
public List<UserModuleVo> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<UserModuleVo> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,37 @@ import com.yunhai.notice.entity.ContactsGroupUser;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ContactsGroupUserRepository extends JpaRepository<ContactsGroupUser, Integer> {
|
||||
Page<ContactsGroupUser> findByGroupId(Integer groupId, Pageable pageable);
|
||||
List<ContactsGroupUser> findByGroupId(Integer groupId);
|
||||
void deleteByGroupId(Integer groupId);
|
||||
|
||||
/**
|
||||
* 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。
|
||||
* 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行,
|
||||
* 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。
|
||||
*/
|
||||
@Modifying(clearAutomatically = true)
|
||||
@Query("delete from ContactsGroupUser g where g.groupId = :groupId")
|
||||
void deleteByGroupIdBulk(@Param("groupId") Integer groupId);
|
||||
|
||||
/**
|
||||
* 按多个组ID批量删除成员。
|
||||
*/
|
||||
@Modifying(clearAutomatically = true)
|
||||
@Query("delete from ContactsGroupUser g where g.groupId in :groupIds")
|
||||
void deleteByGroupIdInBulk(@Param("groupIds") List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按多个组ID查询所有成员(用于统计每组人数、组树构建)。
|
||||
*/
|
||||
List<ContactsGroupUser> findByGroupIdIn(List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按多个组ID批量删除成员。
|
||||
*/
|
||||
void deleteByGroupIdIn(List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 按组ID和用户ID查询(判断是否已存在)。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.yunhai.notice.repository;
|
||||
|
||||
import com.yunhai.notice.entity.NoticeReceiver;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知接收人关联表仓储。
|
||||
*/
|
||||
public interface NoticeReceiverRepository extends JpaRepository<NoticeReceiver, Integer> {
|
||||
|
||||
/** 按通知ID查询全部接收人 */
|
||||
List<NoticeReceiver> findByNoticeId(Integer noticeId);
|
||||
|
||||
/** 按通知ID删除全部接收人 */
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
|
||||
/** 按通知ID和接收类型查询接收人 */
|
||||
List<NoticeReceiver> findByNoticeIdAndReceiveType(Integer noticeId, Integer receiveType);
|
||||
|
||||
/**
|
||||
* 批量查 N 个通知的接收人,用于列表展示(避免 N+1)。
|
||||
* @param noticeIds 通知ID列表
|
||||
* @return 接收人关联记录列表
|
||||
*/
|
||||
@Query("select r from NoticeReceiver r where r.noticeId in :noticeIds")
|
||||
List<NoticeReceiver> findByNoticeIds(@Param("noticeIds") List<Integer> noticeIds);
|
||||
}
|
||||
@@ -16,10 +16,18 @@ public interface NoticeRepository extends JpaRepository<Notice, Integer>,
|
||||
JpaSpecificationExecutor<Notice> {
|
||||
|
||||
/**
|
||||
* 按状态计数(用于统计:已发布/草稿总数)。
|
||||
* 按状态计数(用于统计已发布总数)。
|
||||
*/
|
||||
long countByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 已发布且已到点(定时发送未到点不计入)的通知数,用于用户端未读数统计。
|
||||
*/
|
||||
@Query("select count(n) from Notice n where n.status = :status " +
|
||||
"and (n.timingSendTime is null or n.timingSendTime <= :now)")
|
||||
long countPublishedDue(@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询指定状态、指定时间范围内的通知创建时间(用于按月统计,避免加载大字段)。
|
||||
*/
|
||||
@@ -39,10 +47,43 @@ public interface NoticeRepository extends JpaRepository<Notice, Integer>,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 查询待定时发送的通知:状态为草稿、timingSendTime 不为空且 <= 当前时间。
|
||||
* 按学校和状态计数(多租户隔离统计)。
|
||||
*/
|
||||
@Query("select n from Notice n where n.status = :status " +
|
||||
"and n.timingSendTime is not null and n.timingSendTime <= :now")
|
||||
List<Notice> findPendingTimingSend(@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
long countBySchIdAndStatus(Integer schId, Integer status);
|
||||
|
||||
/**
|
||||
* 按学校计数(多租户隔离统计:全部状态)。
|
||||
*/
|
||||
long countBySchId(Integer schId);
|
||||
|
||||
/**
|
||||
* 已发布且已到点(定时发送未到点不计入)且属于指定学校的通知数(用户端未读数统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select count(n) from Notice n where n.schId = :schId and n.status = :status " +
|
||||
"and (n.timingSendTime is null or n.timingSendTime <= :now)")
|
||||
long countPublishedDueBySchId(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询指定学校、指定状态、指定时间范围内的通知创建时间(按月统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select n.createTime from Notice n " +
|
||||
"where n.schId = :schId and n.status = :status " +
|
||||
"and n.createTime >= :start and n.createTime <= :end")
|
||||
List<LocalDateTime> findCreateTimesBySchIdStatusAndRange(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 查询指定学校、指定状态、指定时间范围内的发布人姓名(按发布人统计,多租户隔离)。
|
||||
*/
|
||||
@Query("select n.publisherName from Notice n " +
|
||||
"where n.schId = :schId and n.status = :status " +
|
||||
"and n.createTime >= :start and n.createTime <= :end")
|
||||
List<String> findPublisherNamesBySchStatusAndRange(@Param("schId") Integer schId,
|
||||
@Param("status") Integer status,
|
||||
@Param("start") LocalDateTime start,
|
||||
@Param("end") LocalDateTime end);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import com.yunhai.notice.entity.SysDict;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SysDictRepository extends JpaRepository<SysDict, Integer> {
|
||||
List<SysDict> findByDictTypeOrderBySortOrderAsc(String dictType);
|
||||
List<SysDict> findByDictTypeAndEnableFlagOrderBySortOrderAsc(String dictType, Integer enableFlag);
|
||||
|
||||
/** 按类型+key 取单条字典(发文部门名称解析用) */
|
||||
Optional<SysDict> findByDictTypeAndDictKey(String dictType, String dictKey);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.yunhai.notice.entity.UpFile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 附件文件仓储。
|
||||
*/
|
||||
@@ -15,6 +14,11 @@ public interface UpFileRepository extends JpaRepository<UpFile, Integer> {
|
||||
*/
|
||||
List<UpFile> findByTopicTypeAndTopicId(String topicType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 按主题类型和多个主题ID批量查询附件(消除 N+1)。
|
||||
*/
|
||||
List<UpFile> findByTopicTypeAndTopicIdIn(String topicType, List<Integer> topicIds);
|
||||
|
||||
/**
|
||||
* 按主题类型和主题ID删除附件(通知删除时级联清理)。
|
||||
*/
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务接口。
|
||||
*/
|
||||
public interface FileService {
|
||||
|
||||
/**
|
||||
* 上传单个文件。
|
||||
*
|
||||
* @param file 上传的文件
|
||||
* @param appType 应用类型(如 "notice")
|
||||
* @param topicId 关联主题ID(可为空,通知保存后再绑定)
|
||||
* @return 文件视图对象
|
||||
*/
|
||||
UpFileVo upload(MultipartFile file, String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 将已上传但未绑定主题的附件绑定到指定通知。
|
||||
*
|
||||
* @param fileIds 文件记录ID列表
|
||||
* @param topicId 通知ID
|
||||
*/
|
||||
void bindToTopic(List<Integer> fileIds, Integer topicId);
|
||||
|
||||
/**
|
||||
* 查询某主题下的附件列表。
|
||||
*
|
||||
* @param appType 应用类型
|
||||
* @param topicId 主题ID
|
||||
* @return 附件列表
|
||||
*/
|
||||
List<UpFileVo> listByTopic(String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 下载文件。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void download(Integer fileId, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 删除文件(同时删除物理文件和数据库记录)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
*/
|
||||
void delete(Integer fileId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import com.yunhai.notice.model.SchTeaList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理(bschclass)开放接口客户端。
|
||||
* 基址取配置 {@code yh.open.bschclass},当前仅提供教师查询,后续班级/年级等接口按需追加。
|
||||
*/
|
||||
public interface IClassService {
|
||||
|
||||
/**
|
||||
* 获取指定学校全部教师(含所属组 GroupIds)。
|
||||
* 对应 bschclass 开放接口:GET {@code {bschclass}/oapi/DbClass/GetSchteaList?Group={schId}}
|
||||
*
|
||||
* @param schId 学校ID(即用户当前身份的 schId)
|
||||
*/
|
||||
List<SchTeaList> getSchTeaList(int schId);
|
||||
}
|
||||
+1
-17
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
* 联系组业务接口。
|
||||
* 对应前端 Contacts 组接口:AddGroup/GetGroup/EditGroup/DelGroup/SetContactsTea/GetContactsTea/GetGroupTree/GetDeptRoleGroupTree/Import/Export。
|
||||
*/
|
||||
public interface ContactsService {
|
||||
public interface IContactsService {
|
||||
|
||||
/**
|
||||
* 添加联系组。
|
||||
@@ -76,20 +76,4 @@ public interface ContactsService {
|
||||
* @return 树形结构 List<Map>
|
||||
*/
|
||||
List<Map<String, Object>> getDeptRoleGroupTree();
|
||||
|
||||
/**
|
||||
* 导入联系组(Excel)。
|
||||
* C# 读取Excel文件,逐行匹配组名+教师。Java版预留。
|
||||
*
|
||||
* @return 导入结果 Map
|
||||
*/
|
||||
Map<String, Object> importContacts();
|
||||
|
||||
/**
|
||||
* 导出联系组(Excel)。
|
||||
* C# 生成Excel文件流返回。Java版预留。
|
||||
*
|
||||
* @return 导出数据 Map
|
||||
*/
|
||||
Map<String, Object> exportContacts();
|
||||
}
|
||||
+10
-1
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
* 系统字典业务接口。
|
||||
* 对应前端 Base 组接口:GetSysDic/GetDictMangeList/AddDeptDic/UpdateDeptDic/DelDept/OrdbyDicIds/GetDeptSync/GetSchModuleIsSms。
|
||||
*/
|
||||
public interface DictService {
|
||||
public interface IDictService {
|
||||
|
||||
/**
|
||||
* 获取系统字典(下拉框用)。
|
||||
@@ -67,4 +67,13 @@ public interface DictService {
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean getSchModuleIsSms();
|
||||
|
||||
/**
|
||||
* 按发文部门字典 key(CodeID)解析部门名称;未命中返回 null。
|
||||
* 用于通知列表/详情展示 DeptName(对应 C# GetDeptName)。
|
||||
*
|
||||
* @param deptId 通知 deptId(存的是 sys_dict.dict_key)
|
||||
* @return 部门名称;无匹配时返回 null
|
||||
*/
|
||||
String resolveDeptName(String deptId);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务接口。
|
||||
*/
|
||||
public interface IFileService {
|
||||
/**
|
||||
* 上传单个文件。
|
||||
*
|
||||
* @param file 上传的文件
|
||||
* @param appType 应用类型(如 "notice")
|
||||
* @param topicId 关联主题ID(可为空,通知保存后再绑定)
|
||||
* @return 文件视图对象
|
||||
*/
|
||||
UpFileVo upload(MultipartFile file, String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 将已上传但未绑定主题的附件绑定到指定通知。
|
||||
*
|
||||
* @param fileIds 文件记录ID列表
|
||||
* @param topicId 通知ID
|
||||
*/
|
||||
void bindToTopic(List<Integer> fileIds, Integer topicId);
|
||||
|
||||
/**
|
||||
* 查询某主题下的附件列表。
|
||||
*
|
||||
* @param appType 应用类型
|
||||
* @param topicId 主题ID
|
||||
* @return 附件列表
|
||||
*/
|
||||
List<UpFileVo> listByTopic(String appType, Integer topicId);
|
||||
|
||||
/**
|
||||
* 下载文件。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void download(Integer fileId, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 在线预览文件(Content-Disposition: inline,按扩展名输出真实 Content-Type,
|
||||
* 供前端 open-file-viewer 取 Blob / video 播放)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void preview(Integer fileId, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 删除文件(同时删除物理文件和数据库记录)。
|
||||
*
|
||||
* @param fileId 文件记录ID
|
||||
*/
|
||||
void delete(Integer fileId);
|
||||
|
||||
/**
|
||||
* 打包下载多个附件为 ZIP。
|
||||
*
|
||||
* @param files 附件记录列表(调用方按 fileIds 查好)
|
||||
* @param zipName 压缩包显示名(自动追加 .zip)
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 按主题类型和主题ID打包下载附件为 ZIP(Controller 便捷方法,内部自行查附件列表)。
|
||||
*
|
||||
* @param topicType 主题类型(如 "notice")
|
||||
* @param topicId 主题ID
|
||||
* @param zipName 压缩包显示名
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
void downloadZipByTopic(String topicType, Integer topicId, String zipName, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 批量上传图片(循环上传,返回每个文件的视图对象列表)。
|
||||
*/
|
||||
List<UpFileVo> uploadImages(MultipartFile[] files, String appType);
|
||||
|
||||
/**
|
||||
* 上传单个文件并返回前端兼容的结果 VO(含 fileid/name/fileurl/host/type/size)。
|
||||
*/
|
||||
UpFileResultVo uploadToResult(MultipartFile file, String appType);
|
||||
}
|
||||
@@ -2,15 +2,13 @@ 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 com.yunhai.notice.model.notice.*;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通知公告业务接口。
|
||||
@@ -19,14 +17,12 @@ public interface INoticeService {
|
||||
|
||||
/**
|
||||
* 发通知:以当前登录用户为发布人,状态置为「已发布」入库。
|
||||
* 若 request.timingSendTime 不为空且晚于当前时间,则置为草稿等待定时发送。
|
||||
* 若 request.timingSendTime 不为空且晚于当前时间,则作为定时发送,
|
||||
* 未到点前用户端收件箱不可见(到点后由查询过滤自然可见)。
|
||||
*
|
||||
* @return 新建通知 ID(用于后续回填附件 TopicID 等)
|
||||
*/
|
||||
void publish(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 保存草稿:状态置为「草稿」,不发布。
|
||||
*/
|
||||
Integer saveDraft(NoticeRequest request);
|
||||
Integer publish(NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 编辑通知:按 id 更新标题/内容/类型/部门/置顶,发布人与状态保持不变。
|
||||
@@ -34,9 +30,9 @@ public interface INoticeService {
|
||||
void edit(Integer id, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 删除通知:删除主记录并级联清理其阅读记录。
|
||||
* 批量删除通知:删除主记录并级联清理阅读记录、接收人、附件关联。
|
||||
*/
|
||||
void delete(Integer id);
|
||||
void deleteBatch(List<Integer> ids);
|
||||
|
||||
/**
|
||||
* 撤回通知:仅已发布且未撤回的通知可撤回,置 isPulled=1。
|
||||
@@ -49,19 +45,15 @@ public interface INoticeService {
|
||||
void setTopmost(Integer id, Integer topmost);
|
||||
|
||||
/**
|
||||
* 转发通知:基于源通知创建新草稿,parentId 关联源通知。
|
||||
* 转发通知:基于源通知创建新通知(已发布),parentId 关联源通知。
|
||||
*/
|
||||
Integer forward(Integer sourceId, NoticeRequest request);
|
||||
|
||||
/**
|
||||
* 发送通知(供定时任务和手动发送调用):更新状态为已发布,异步推送消息。
|
||||
* 用户端列表:仅展示已发布且已到点的通知,并标记当前用户是否已读,附未读总数。
|
||||
* 返回 NoticePageResult(扩展 unreadCount)。
|
||||
*/
|
||||
void sendNotify(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 用户端列表:仅展示已发布通知,并标记当前用户是否已读,附未读总数。
|
||||
*/
|
||||
NoticeUserListResult userList(NoticeQuery query, Pageable pageable);
|
||||
NoticePageResult<NoticeVo> userList(NoticeQuery query, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 管理端列表:按条件查询全部通知(管理员查看全部)。
|
||||
@@ -73,18 +65,78 @@ public interface INoticeService {
|
||||
*/
|
||||
NoticeVo detail(Integer id);
|
||||
|
||||
/**
|
||||
* 编辑回显详情(不标记已读)。
|
||||
*/
|
||||
NoticeVo getDetail(Integer id);
|
||||
|
||||
/**
|
||||
* 标记已读:为当前用户写入(幂等)该通知的阅读记录。
|
||||
*/
|
||||
void markRead(Integer id);
|
||||
|
||||
/**
|
||||
* 一键全部已读(批量化):当前用户所有未读的已发布通知批量插入阅读记录。
|
||||
* @return 已读条数
|
||||
*/
|
||||
int initializeRed();
|
||||
|
||||
/**
|
||||
* 追加接收人(事务化):并入接收人列表并重算 allowcount。
|
||||
*/
|
||||
void receiveUsers(Integer noticeId, String users);
|
||||
|
||||
/**
|
||||
* 上一篇/下一篇通知ID(SQL 相邻查询)。
|
||||
* @param serid 当前通知ID
|
||||
* @param type 0 已发(当前用户发布的序列)/ 1 已收(阅读记录序列)
|
||||
* @return {prevId, nextId}
|
||||
*/
|
||||
Map<String, Object> getNextPrev(Integer serid, String type);
|
||||
|
||||
/**
|
||||
* 获取某通知的阅读明细列表。
|
||||
*/
|
||||
List<NoticeVo.ReadDetail> getReadDetail(Integer noticeId);
|
||||
|
||||
/**
|
||||
* 通知统计:总数/已发布/草稿、累计阅读人次、当前用户未读、按发布人、按月。
|
||||
* 阅读情况分组(已读/未读)。
|
||||
*/
|
||||
List<Map<String, Object>> getReadDetailGrouped(Integer serid);
|
||||
|
||||
/**
|
||||
* 催收未读:枚举未读接收人,后续接入消息推送。
|
||||
*/
|
||||
void expedite(Integer serid);
|
||||
|
||||
/**
|
||||
* 通知统计:总数/已发布、累计阅读人次、当前用户未读、按发布人、按月。
|
||||
*/
|
||||
NoticeStatisticsVo statistics(NoticeQuery query, LocalDateTime start, LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 通知详情视图(含附件、接收人信息),供 Controller 返回。
|
||||
*/
|
||||
NoticeDetailVo getDetailMap(Integer serid);
|
||||
|
||||
/**
|
||||
* 已发列表附件批量查询(消除 N+1)。
|
||||
*/
|
||||
Map<Integer, List<Integer>> batchFileIds(List<Integer> noticeIds);
|
||||
|
||||
/**
|
||||
* 查看通知详情视图(含附件、接收人)。
|
||||
* type=1 时先标记已读再返回,type=0 时直接返回不标记。
|
||||
*/
|
||||
NoticeDetailVo detailMap(Integer serid, Integer type);
|
||||
|
||||
/**
|
||||
* 一键全部已读并返回结果(count = 已读条数)。
|
||||
*/
|
||||
InitializeRedVo initializeRedResult();
|
||||
|
||||
/**
|
||||
* 批量下载通知全部附件(打包 zip):查通知标题后调文件域打包下载。
|
||||
*/
|
||||
void downloadAllFiles(Integer dataid, HttpServletResponse response);
|
||||
}
|
||||
|
||||
+7
-1
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
* 短信设置业务接口。
|
||||
* 对应前端 setSms 组接口:getmodule/getgroup/getschsms/getsch/setschsms/setschallsms/setschmodulesms。
|
||||
*/
|
||||
public interface SmsService {
|
||||
public interface ISmsService {
|
||||
|
||||
/**
|
||||
* 获取短信模块列表。
|
||||
@@ -68,4 +68,10 @@ public interface SmsService {
|
||||
* @param isSms 是否开通
|
||||
*/
|
||||
void setSchModuleSms(String schSerIDs, String modules, Boolean isSms);
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串解析并设置单校单模块短信开关。
|
||||
* 解析 SchSerID/ModuID/IsSms 三个字段。
|
||||
*/
|
||||
void setSchSmsFromJson(String itemsModuJson);
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
* 通知模板业务接口。
|
||||
* 对应前端 Temp 组接口:GetTypelist/EditType/DelType/GetTemplatelist/EditTemplate/DelTemplate/GetTemplateFist。
|
||||
*/
|
||||
public interface TemplateService {
|
||||
public interface ITemplateService {
|
||||
|
||||
/**
|
||||
* 获取模板类型列表(首项固定为"全部")。
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.yunhai.notice.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户中心门面服务(对齐 UserBase {@code UserCenterService} 模式)。
|
||||
* <p>封装 {@code YschBaseOpenApi} / {@code OpenApiUserClient} 静态门面调用,
|
||||
* 把原本散落在 Controller 的组树组装、教师挂组、成员解析等业务逻辑下沉到 Service 层,
|
||||
* Controller 只保留"取参 → 调 Service → 返回"的薄壳。</p>
|
||||
*/
|
||||
public interface IUserCenterService {
|
||||
|
||||
/**
|
||||
* 获取当前用户学校组及用户(树形结构)。
|
||||
* <p>组装规则(从原 BaseApiController.getSchoolAll 下沉):</p>
|
||||
* <ol>
|
||||
* <li>userbase 读取当前学校整棵组树(getGroupById(schId, withSub=true)),剔除学生组(type=8)及其子组;</li>
|
||||
* <li>bschclass 获取该校全部教师(/oapi/DbClass/GetSchteaList?Group=schId),按 GroupIds 挂到对应组下,
|
||||
* 无有效组(如所属组全被剔除)的教师兜底挂学校根节点;</li>
|
||||
* <li>扁平节点按 pid 递归组装为树(空组剔除,同 C# FormatUserTree)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param schId 学校ID(用户当前角色所属学校)
|
||||
* @return 用户树形结构(组节点 type=1 / 教师节点 type=2)
|
||||
*/
|
||||
List<Map<String, Object>> getSchoolAllTree(Integer schId);
|
||||
|
||||
/**
|
||||
* 获取指定组内用户(教师 + 管理员)。
|
||||
* <p>对应前端 base/GetGroupUser。原实现为空桩返回空列表,现按 C# 语义
|
||||
* (ITeainfoBLL.GetGroupTeainfo + IAdminfoBLL.GetGroupAdminfo)接入用户中心。
|
||||
* 若用户中心不可用或未配置对应开放接口,则抛出带语义的异常(显式报错而非静默失能)。</p>
|
||||
*
|
||||
* @param gid 组ID
|
||||
* @param role 角色(默认 2=教师;1=管理员等)
|
||||
* @return 组内用户列表
|
||||
*/
|
||||
List<Map<String, Object>> getGroupUser(Integer gid, Integer role);
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的模块树(左侧菜单)。
|
||||
* <p>从基础平台拉取当前用户模块列表(含学校上下文),组装为前端 layout 期望的树结构
|
||||
* ({@code modId/page/children}),对应前端 GetUserMod 接口。</p>
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param schId 学校ID(可为 null)
|
||||
* @return 模块树(顶层 List,含 children 嵌套)
|
||||
*/
|
||||
List<Map<String, Object>> getUserModuleTree(Integer userId, Integer schId);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.model.SchTeaList;
|
||||
import com.yunhai.notice.service.IClassService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级管理(bschclass)开放接口实现。
|
||||
* bschclass 为开放接口(无需应用签名),使用独立 RestTemplate 直连。
|
||||
*/
|
||||
@Service
|
||||
public class ClassService implements IClassService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ClassService.class);
|
||||
|
||||
/** 班级管理服务基址,如 http://base.yunhaixingtong.com/bschclass */
|
||||
@Value("${yh.open.bschclass:}")
|
||||
private String bschClassHost;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public ClassService() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(5000);
|
||||
factory.setReadTimeout(10000);
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SchTeaList> getSchTeaList(int schId) {
|
||||
if (!StringUtils.hasText(bschClassHost)) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "班级管理接口未配置(yh.open.bschclass)");
|
||||
}
|
||||
String url = bschClassHost + "/oapi/DbClass/GetSchteaList?Group=" + schId;
|
||||
try {
|
||||
ResponseEntity<List<SchTeaList>> resp = restTemplate.exchange(
|
||||
url, HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<List<SchTeaList>>() {
|
||||
});
|
||||
List<SchTeaList> list = resp.getBody();
|
||||
log.info("[getSchTeaList] schId={}, 获取教师 {} 人", schId, list == null ? 0 : list.size());
|
||||
return list != null ? list : Collections.emptyList();
|
||||
} catch (RestClientException e) {
|
||||
log.error("[getSchTeaList] 请求班级管理接口失败, schId={}, url={}", schId, url, e);
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "获取教师数据失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
-105
@@ -1,12 +1,16 @@
|
||||
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 com.yunhai.notice.service.IContactsService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -24,9 +28,9 @@ import java.util.stream.Collectors;
|
||||
* 对应 C# ContactsController + ManageContacts.cs。
|
||||
*/
|
||||
@Service
|
||||
public class ContactsServiceImpl implements ContactsService {
|
||||
public class ContactsService implements IContactsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ContactsServiceImpl.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(ContactsService.class);
|
||||
|
||||
@Autowired
|
||||
private ContactsGroupRepository groupRepository;
|
||||
@@ -40,19 +44,19 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
@Transactional
|
||||
public void addGroup(String name, Integer type) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new NoticeException("请输入自定义组的名字");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "请输入自定义组的名字");
|
||||
}
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
|
||||
// 查重:同创建人下同名同类型组不重复
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该联系组!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组!");
|
||||
}
|
||||
|
||||
ContactsGroup group = new ContactsGroup();
|
||||
@@ -68,7 +72,7 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
|
||||
// 查询当前用户创建的全部联系组
|
||||
List<ContactsGroup> allGroups = groupRepository.findByCreatorAccount(account);
|
||||
@@ -113,23 +117,23 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
@Transactional
|
||||
public void editGroup(Integer id, String name, Integer type) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new NoticeException("联系组名称不能为空");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "联系组名称不能为空");
|
||||
}
|
||||
if (id == null) {
|
||||
throw new NoticeException("参数错误!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
|
||||
// 查重(排除自身)
|
||||
List<ContactsGroup> existing = groupRepository.findByCreatorAccount(account);
|
||||
boolean duplicate = existing.stream().anyMatch(g ->
|
||||
!g.getId().equals(id) && name.equals(g.getGroupName()));
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该联系组");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该联系组");
|
||||
}
|
||||
|
||||
ContactsGroup group = groupRepository.findById(id)
|
||||
.orElseThrow(() -> new NoticeException("联系组不存在"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "联系组不存在"));
|
||||
group.setGroupName(name);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
@@ -140,7 +144,7 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
@Transactional
|
||||
public void delGroup(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new NoticeException("参数错误,删除失败");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误,删除失败");
|
||||
}
|
||||
List<Integer> idList = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
@@ -149,7 +153,7 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 先删成员再删组
|
||||
groupUserRepository.deleteByGroupIdIn(idList);
|
||||
groupUserRepository.deleteByGroupIdInBulk(idList);
|
||||
groupRepository.findAllById(idList).forEach(groupRepository::delete);
|
||||
}
|
||||
|
||||
@@ -159,21 +163,34 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
@Transactional
|
||||
public void setContactsTea(Integer gid, String ids) {
|
||||
if (gid == null || gid == 0) {
|
||||
throw new NoticeException("参数错误");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
// 全量替换:先删除该组全部成员,再新增
|
||||
groupUserRepository.deleteByGroupId(gid);
|
||||
// 全量替换:bulk delete 立即执行 SQL,避免 derived delete 延迟 flush
|
||||
// 导致同事务 insert 先行触发 uk_group_user(参考 PartyLearn GroupService.memberEdit)
|
||||
groupUserRepository.deleteByGroupIdBulk(gid);
|
||||
|
||||
if (StringUtils.hasText(ids)) {
|
||||
List<Integer> userIds = Arrays.stream(ids.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Integer::parseInt)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
for (Integer userId : userIds) {
|
||||
ContactsGroupUser member = new ContactsGroupUser();
|
||||
member.setGroupId(gid);
|
||||
member.setUserId(userId);
|
||||
// 姓名/账号快照落库(对齐 PartyLearn memberEdit 保存 userName 的做法),
|
||||
// 查询时 fillUserInfo 仅对缺失值兜底,避免组树/分页反复远程调用
|
||||
try {
|
||||
BoaUser user = YschBaseOpenApi.getUserInfo(userId);
|
||||
if (user != null) {
|
||||
member.setUserAccount(user.getAccount());
|
||||
member.setUserName(user.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[setContactsTea] 查询用户信息失败, userId={}", userId, e);
|
||||
}
|
||||
groupUserRepository.save(member);
|
||||
}
|
||||
}
|
||||
@@ -199,32 +216,73 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
List<ContactsGroupUser> allMembers = groupUserRepository.findByGroupId(groupId);
|
||||
int total = allMembers.size();
|
||||
|
||||
List<Map<String, Object>> listData = allMembers.stream().map(m -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("sid", m.getId());
|
||||
item.put("id", m.getUserId());
|
||||
item.put("name", m.getUserName() != null ? m.getUserName() : "");
|
||||
item.put("account", m.getUserAccount() != null ? m.getUserAccount() : "");
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 分页
|
||||
// 先分页再补全用户信息(避免为未分页数据调用用户中心)
|
||||
List<ContactsGroupUser> pageMembers = allMembers;
|
||||
if (count != null && count > 0 && page != null && page > 0) {
|
||||
int fromIndex = (page - 1) * count;
|
||||
int toIndex = Math.min(fromIndex + count, total);
|
||||
if (fromIndex < total) {
|
||||
listData = listData.subList(fromIndex, toIndex);
|
||||
} else {
|
||||
listData = Collections.emptyList();
|
||||
}
|
||||
pageMembers = fromIndex < total ? allMembers.subList(fromIndex, toIndex) : Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> listData = pageMembers.stream().map(m -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("sid", m.getId());
|
||||
item.put("id", m.getUserId());
|
||||
fillUserInfo(item, m);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", listData);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全成员的账号/姓名:落库值为空时从用户中心按 userId 反查。
|
||||
* 对应 C# 保存成员时同步写入 UserName/UserAccount 的行为。
|
||||
* 查到的值会回写落库自愈,避免历史空数据每次读取都重复远程调用。
|
||||
*/
|
||||
private void fillUserInfo(Map<String, Object> item, ContactsGroupUser m) {
|
||||
String name = m.getUserName();
|
||||
String account = m.getUserAccount();
|
||||
if (m.getUserId() != null && (!StringUtils.hasText(name) || !StringUtils.hasText(account))) {
|
||||
BoaUser user = null;
|
||||
try {
|
||||
user = YschBaseOpenApi.getUserInfo(m.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 查询用户中心用户失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
if (user == null) {
|
||||
log.warn("[fillUserInfo] 用户中心未查到用户(历史脏数据?), userId={}", m.getUserId());
|
||||
} else {
|
||||
if (!StringUtils.hasText(account)) {
|
||||
account = user.getAccount();
|
||||
}
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = user.getName();
|
||||
}
|
||||
// 回写落库:下次读取直接命中快照
|
||||
if ((!StringUtils.hasText(m.getUserAccount()) && StringUtils.hasText(account))
|
||||
|| (!StringUtils.hasText(m.getUserName()) && StringUtils.hasText(name))) {
|
||||
if (StringUtils.hasText(account)) {
|
||||
m.setUserAccount(account);
|
||||
}
|
||||
if (StringUtils.hasText(name)) {
|
||||
m.setUserName(name);
|
||||
}
|
||||
try {
|
||||
groupUserRepository.save(m);
|
||||
} catch (Exception e) {
|
||||
log.warn("[fillUserInfo] 回写用户信息失败, userId={}", m.getUserId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item.put("name", name != null ? name : "");
|
||||
item.put("account", account != null ? account : "");
|
||||
}
|
||||
|
||||
// ============================ GetGroupTree ============================
|
||||
|
||||
@Override
|
||||
@@ -232,7 +290,7 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
if (type == null) {
|
||||
type = 1;
|
||||
}
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
|
||||
// 查询当前用户的联系组
|
||||
List<ContactsGroup> groups = groupRepository.findByCreatorAccount(account);
|
||||
@@ -253,35 +311,28 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
node.put("pid", 0);
|
||||
node.put("no", 0);
|
||||
node.put("type", 1);
|
||||
node.put("isParent", true);
|
||||
node.put("photo", "");
|
||||
node.put("children", new ArrayList<Map<String, Object>>());
|
||||
treeNodes.add(node);
|
||||
}
|
||||
|
||||
// 用户节点 (type=2, pid=groupId)
|
||||
// 用户节点 (type=2, pid=groupId),姓名缺失时从用户中心补全
|
||||
for (ContactsGroupUser m : allMembers) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", m.getUserId());
|
||||
node.put("name", m.getUserName() != null ? m.getUserName() : "");
|
||||
node.put("name", "");
|
||||
node.put("pid", m.getGroupId());
|
||||
node.put("no", 0);
|
||||
node.put("type", 2);
|
||||
node.put("isParent", false);
|
||||
node.put("photo", "");
|
||||
fillUserInfo(node, m);
|
||||
treeNodes.add(node);
|
||||
}
|
||||
|
||||
// 递归构建树
|
||||
List<Map<String, Object>> tree = formatUserTree(treeNodes, 0);
|
||||
|
||||
// 包裹根节点
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("id", 0);
|
||||
root.put("name", "通知公告系统");
|
||||
root.put("children", tree);
|
||||
root.put("no", "0");
|
||||
root.put("type", 1);
|
||||
|
||||
return Collections.singletonList(root);
|
||||
// 递归构建树:各自定义组为顶级节点(空组自动剔除),组合成森林
|
||||
return formatUserTree(treeNodes, 0);
|
||||
}
|
||||
|
||||
// ============================ GetDeptRoleGroupTree ============================
|
||||
@@ -294,61 +345,6 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ============================ Import ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> importContacts() {
|
||||
// C# 读取上传的Excel文件逐行匹配组名+教师。
|
||||
// Java版暂未实现Excel解析,预留接口。
|
||||
log.info("[importContacts] Excel导入功能暂未实现");
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("msg", "导入功能暂未实现");
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ Export ============================
|
||||
|
||||
@Override
|
||||
public Map<String, Object> exportContacts() {
|
||||
// C# 生成Excel文件流返回。
|
||||
// Java版暂未实现Excel导出,预留接口。
|
||||
log.info("[exportContacts] Excel导出功能暂未实现");
|
||||
|
||||
// 返回联系组数据供前端参考
|
||||
String account = NoticeContext.getAccount();
|
||||
List<ContactsGroup> groups = groupRepository.findByCreatorAccount(account);
|
||||
List<Integer> groupIds = groups.stream().map(ContactsGroup::getId).collect(Collectors.toList());
|
||||
List<ContactsGroupUser> allMembers = groupIds.isEmpty() ? Collections.emptyList()
|
||||
: groupUserRepository.findByGroupIdIn(groupIds);
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (ContactsGroup g : groups) {
|
||||
List<ContactsGroupUser> members = allMembers.stream()
|
||||
.filter(m -> m.getGroupId().equals(g.getId()))
|
||||
.collect(Collectors.toList());
|
||||
if (members.isEmpty()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("组名", g.getGroupName());
|
||||
row.put("教师", "");
|
||||
row.put("用户名", "");
|
||||
rows.add(row);
|
||||
} else {
|
||||
for (ContactsGroupUser m : members) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("组名", g.getGroupName());
|
||||
row.put("教师", m.getUserName() != null ? m.getUserName() : "");
|
||||
row.put("用户名", m.getUserAccount() != null ? m.getUserAccount() : "");
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", rows);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
@@ -387,7 +383,10 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
}
|
||||
|
||||
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||
node.put("children", children);
|
||||
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||
if (itemType == 1) {
|
||||
node.put("children", children);
|
||||
}
|
||||
result.add(node);
|
||||
}
|
||||
|
||||
@@ -403,4 +402,12 @@ public class ContactsServiceImpl implements ContactsService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
|
||||
*/
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
+41
-18
@@ -1,12 +1,14 @@
|
||||
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 com.yunhai.notice.service.IDictService;
|
||||
import com.yunhai.platform.auth.LoginUser;
|
||||
import com.yunhai.platform.auth.UserContextHolder;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -22,9 +24,9 @@ import java.util.stream.Collectors;
|
||||
* 对应 C# CommonController + GetSysDict.cs 中的字典管理逻辑。
|
||||
*/
|
||||
@Service
|
||||
public class DictServiceImpl implements DictService {
|
||||
public class DictService implements IDictService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DictServiceImpl.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(DictService.class);
|
||||
|
||||
/** 字典类型常量:发文部门 */
|
||||
private static final String DICT_TYPE_DEPT = "dept";
|
||||
@@ -42,11 +44,11 @@ public class DictServiceImpl implements DictService {
|
||||
// C# 中 Type=1 为部门类别(DEPT),其余类型(文印、请假等)本系统暂不涉及
|
||||
String dictType = mapDictType(type);
|
||||
if (dictType == null) {
|
||||
throw new NoticeException("参数错误");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(dictType, 1);
|
||||
if (list.isEmpty()) {
|
||||
throw new NoticeException("尚未添加发文部门,请联系管理员添加!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "尚未添加发文部门,请联系管理员添加!");
|
||||
}
|
||||
// 按 sortOrder 升序返回 {k, v}
|
||||
return list.stream()
|
||||
@@ -68,7 +70,7 @@ public class DictServiceImpl implements DictService {
|
||||
String dictType = "dept".equalsIgnoreCase(type) ? DICT_TYPE_DEPT : null;
|
||||
if (dictType == null) {
|
||||
// 非 DEPT 类型需超管权限,本系统暂只支持 DEPT
|
||||
throw new NoticeException("非超管不得操作!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "非超管不得操作!");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(dictType);
|
||||
return list.stream()
|
||||
@@ -91,14 +93,14 @@ public class DictServiceImpl implements DictService {
|
||||
@Transactional
|
||||
public void addDeptDic(String name) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
throw new NoticeException("输入字符串格式不正确!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "输入字符串格式不正确!");
|
||||
}
|
||||
// 校验重名
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
DICT_TYPE_DEPT.equals(d.getDictType()) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该分类,请重新填写!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
// 计算 sortOrder = max + 1
|
||||
@@ -122,18 +124,18 @@ public class DictServiceImpl implements DictService {
|
||||
@Transactional
|
||||
public void updateDeptDic(Integer serID, String name, Integer enable) {
|
||||
if (serID == null) {
|
||||
throw new NoticeException("参数错误!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误!");
|
||||
}
|
||||
// 校验重名(排除自身)
|
||||
List<SysDict> existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
boolean duplicate = existing.stream().anyMatch(d ->
|
||||
!d.getId().equals(serID) && name.equals(d.getDictValue()) && d.getEnableFlag() != 3);
|
||||
if (duplicate) {
|
||||
throw new NoticeException("已存在该分类,请重新填写!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("字典记录不存在"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
if (StringUtils.hasText(name)) {
|
||||
dict.setDictValue(name);
|
||||
}
|
||||
@@ -149,14 +151,14 @@ public class DictServiceImpl implements DictService {
|
||||
@Transactional
|
||||
public void delDept(Integer serID) {
|
||||
SysDict dict = sysDictRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("字典记录不存在"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在"));
|
||||
// 校验是否被通知引用(按 deptId = dictKey 匹配)
|
||||
// C# 中检查 Notify_Message.DeptID == dict.CodeID
|
||||
// Java 中 Notice.deptId 存的是 dictKey
|
||||
List<Notice> referenced = noticeRepository.findAll((root, query, cb) ->
|
||||
cb.equal(root.get("deptId"), dict.getDictKey()));
|
||||
if (!referenced.isEmpty()) {
|
||||
throw new NoticeException("已引用该部门,无法删除!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "已引用该部门,无法删除!");
|
||||
}
|
||||
sysDictRepository.delete(dict);
|
||||
}
|
||||
@@ -167,11 +169,11 @@ public class DictServiceImpl implements DictService {
|
||||
@Transactional
|
||||
public void ordbyDicIds(String ids) {
|
||||
if (!StringUtils.hasText(ids)) {
|
||||
throw new NoticeException("参数错误");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
List<SysDict> list = sysDictRepository.findByDictTypeOrderBySortOrderAsc(DICT_TYPE_DEPT);
|
||||
if (list.isEmpty()) {
|
||||
throw new NoticeException("字典类型错误,未找到该字典类型");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
// C# 逻辑:取当前最大 Sort 为起始序号,逆序遍历 ids 递减赋值 Des
|
||||
@@ -216,7 +218,7 @@ public class DictServiceImpl implements DictService {
|
||||
public Boolean getSchModuleIsSms() {
|
||||
// C# 通过外部 ISchIsSmsBLL 查询学校短信开关。
|
||||
// Java 版暂不接入外部短信平台,默认返回 false。
|
||||
String account = NoticeContext.getAccount();
|
||||
String account = currentAccount();
|
||||
if (account == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -226,6 +228,19 @@ public class DictServiceImpl implements DictService {
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 按发文部门字典 key 解析部门名称(对应 C# GetDeptName)。
|
||||
*/
|
||||
@Override
|
||||
public String resolveDeptName(String deptId) {
|
||||
if (!StringUtils.hasText(deptId)) {
|
||||
return null;
|
||||
}
|
||||
return sysDictRepository.findByDictTypeAndDictKey(DICT_TYPE_DEPT, deptId.trim())
|
||||
.map(SysDict::getDictValue)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端 Type 参数映射到字典类型字符串。
|
||||
* C# 中使用枚举,Java 版仅支持 DEPT(1)。
|
||||
@@ -238,4 +253,12 @@ public class DictServiceImpl implements DictService {
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人账号(来自平台 UserContextHolder,无登录态时为 null)。
|
||||
*/
|
||||
private String currentAccount() {
|
||||
LoginUser user = UserContextHolder.get();
|
||||
return user == null ? null : user.getLoginName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.model.notice.UpFileResultVo;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.IFileService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务实现。
|
||||
* <p>
|
||||
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
|
||||
*/
|
||||
@Service
|
||||
public class FileService implements IFileService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FileService.class);
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Value("${file.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
private static final String DEFAULT_APP_TYPE = "notice";
|
||||
|
||||
// ============================ 上传 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "上传文件不得为空");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
if (!StringUtils.hasText(originalName)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "文件名为空");
|
||||
}
|
||||
// 提取后缀
|
||||
String ext = "";
|
||||
int dotIdx = originalName.lastIndexOf('.');
|
||||
if (dotIdx >= 0) {
|
||||
ext = originalName.substring(dotIdx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
|
||||
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
|
||||
|
||||
// 物理写入
|
||||
Path fullPath = Paths.get(uploadPath, relativePath);
|
||||
try {
|
||||
Files.createDirectories(fullPath.getParent());
|
||||
file.transferTo(fullPath.toFile());
|
||||
} catch (Exception e) {
|
||||
log.error("[upload] 文件保存失败: {}", fullPath, e);
|
||||
throw new BizException(ErrorCode.ERROR, "文件保存失败", e);
|
||||
}
|
||||
|
||||
// 入库
|
||||
UpFile entity = new UpFile();
|
||||
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType);
|
||||
entity.setTopicId(topicId);
|
||||
entity.setFileTrueName(originalName);
|
||||
entity.setFileName(relativePath);
|
||||
entity.setFileSize((int) file.getSize());
|
||||
entity.setFileType(ext);
|
||||
upFileRepository.save(entity);
|
||||
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
// ============================ 绑定主题 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void bindToTopic(List<Integer> fileIds, Integer topicId) {
|
||||
if (fileIds == null || fileIds.isEmpty() || topicId == null) {
|
||||
return;
|
||||
}
|
||||
for (Integer fileId : fileIds) {
|
||||
upFileRepository.findById(fileId).ifPresent(f -> {
|
||||
f.setTopicId(topicId);
|
||||
upFileRepository.save(f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 查询附件列表 ============================
|
||||
|
||||
@Override
|
||||
public List<UpFileVo> listByTopic(String appType, Integer topicId) {
|
||||
String type = appType == null ? DEFAULT_APP_TYPE : appType;
|
||||
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
|
||||
.map(this::toVo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ 下载 ============================
|
||||
|
||||
@Override
|
||||
public void download(Integer fileId, HttpServletResponse response) {
|
||||
writeFile(fileId, response, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preview(Integer fileId, HttpServletResponse response) {
|
||||
writeFile(fileId, response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件输出。inline=true 时 Content-Disposition 用 inline 并按扩展名输出真实 Content-Type
|
||||
* (浏览器/前端组件可直接渲染),否则 attachment 强制下载。
|
||||
*/
|
||||
private void writeFile(Integer fileId, HttpServletResponse response, boolean inline) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
if (!Files.exists(fullPath)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "物理文件不存在: " + file.getFileName());
|
||||
}
|
||||
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
if (inline) {
|
||||
response.setContentType(resolveContentType(file.getFileName()));
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + encodedName + "\"");
|
||||
} else {
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\"");
|
||||
}
|
||||
response.setContentLengthLong(file.getFileSize());
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
os.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("[writeFile] 文件输出失败: {}", file.getFileName(), e);
|
||||
throw new BizException(ErrorCode.ERROR, "文件下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按扩展名推断 Content-Type(JDK 自带表 + 常见办公/图片格式兜底),未知类型回退流格式。
|
||||
*/
|
||||
private String resolveContentType(String fileName) {
|
||||
String name = String.valueOf(fileName).toLowerCase();
|
||||
Map<String, String> table = new HashMap<>();
|
||||
table.put("pdf", "application/pdf");
|
||||
table.put("png", "image/png");
|
||||
table.put("jpg", "image/jpeg");
|
||||
table.put("jpeg", "image/jpeg");
|
||||
table.put("gif", "image/gif");
|
||||
table.put("bmp", "image/bmp");
|
||||
table.put("webp", "image/webp");
|
||||
table.put("svg", "image/svg+xml");
|
||||
table.put("txt", "text/plain;charset=UTF-8");
|
||||
table.put("mp4", "video/mp4");
|
||||
table.put("webm", "video/webm");
|
||||
table.put("ogg", "video/ogg");
|
||||
table.put("doc", "application/msword");
|
||||
table.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
table.put("xls", "application/vnd.ms-excel");
|
||||
table.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
table.put("ppt", "application/vnd.ms-powerpoint");
|
||||
table.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||
String type = table.get(name.substring(name.lastIndexOf('.') + 1));
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
String guessed = URLConnection.guessContentTypeFromName(name);
|
||||
return guessed != null ? guessed : "application/octet-stream";
|
||||
}
|
||||
|
||||
// ============================ 删除 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer fileId) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "文件不存在: " + fileId));
|
||||
// 删物理文件
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
try {
|
||||
Files.deleteIfExists(fullPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
|
||||
}
|
||||
// 删数据库记录
|
||||
upFileRepository.delete(file);
|
||||
}
|
||||
|
||||
// ============================ 打包下载 ZIP ============================
|
||||
|
||||
@Override
|
||||
public void downloadZip(List<UpFile> files, String zipName, HttpServletResponse response) {
|
||||
if (files == null || files.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "无附件可下载");
|
||||
}
|
||||
try {
|
||||
response.setContentType("application/zip");
|
||||
String name = URLEncoder.encode(zipName == null ? "附件" : zipName, "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + name + ".zip\"");
|
||||
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
|
||||
Set<String> used = new HashSet<>();
|
||||
for (UpFile f : files) {
|
||||
Path p = Paths.get(uploadPath, f.getFileName());
|
||||
if (!Files.exists(p)) {
|
||||
log.warn("[downloadZip] 物理文件缺失,跳过: {}", f.getFileName());
|
||||
continue;
|
||||
}
|
||||
String entryName = sanitizeEntryName(f.getFileTrueName());
|
||||
if (used.contains(entryName)) {
|
||||
entryName = f.getId() + "_" + entryName;
|
||||
}
|
||||
used.add(entryName);
|
||||
zos.putNextEntry(new ZipEntry(entryName));
|
||||
Files.copy(p, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[downloadZip] 打包下载失败", e);
|
||||
throw new BizException(ErrorCode.ERROR, "打包下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 净化 ZIP 条目名:去除路径分隔符等非法字符,防止 Zip Slip。
|
||||
*/
|
||||
private String sanitizeEntryName(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "file";
|
||||
}
|
||||
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
}
|
||||
|
||||
// ============================ 按主题打包下载 ============================
|
||||
|
||||
@Override
|
||||
public void downloadZipByTopic(String topicType, Integer topicId, String zipName,
|
||||
HttpServletResponse response) {
|
||||
String type = topicType == null ? DEFAULT_APP_TYPE : topicType;
|
||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId(type, topicId);
|
||||
downloadZip(files, zipName, response);
|
||||
}
|
||||
|
||||
// ============================ 批量上传图片 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<UpFileVo> uploadImages(MultipartFile[] files, String appType) {
|
||||
List<UpFileVo> results = new ArrayList<>();
|
||||
if (files == null || files.length == 0) {
|
||||
return results;
|
||||
}
|
||||
String type = appType != null ? appType : DEFAULT_APP_TYPE;
|
||||
for (MultipartFile file : files) {
|
||||
results.add(upload(file, type, null));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ============================ 上传(返回前端兼容 VO) ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileResultVo uploadToResult(MultipartFile file, String appType) {
|
||||
UpFileVo vo = upload(file, appType, null);
|
||||
UpFileResultVo result = new UpFileResultVo();
|
||||
result.setFileid(vo.getId());
|
||||
result.setName(vo.getFileTrueName());
|
||||
result.setFileurl(vo.getFileName());
|
||||
result.setHost("");
|
||||
result.setType(vo.getFileType());
|
||||
result.setSize(vo.getFileSize());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 转换 ============================
|
||||
|
||||
private UpFileVo toVo(UpFile entity) {
|
||||
UpFileVo vo = new UpFileVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.common.NoticeException;
|
||||
import com.yunhai.notice.entity.UpFile;
|
||||
import com.yunhai.notice.model.notice.UpFileVo;
|
||||
import com.yunhai.notice.repository.UpFileRepository;
|
||||
import com.yunhai.notice.service.FileService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 文件上传/下载/删除业务实现。
|
||||
* <p>
|
||||
* 存储路径规则:{uploadPath}/{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
* 与 C# UpLoadFile.FileSave 逻辑等价,但去除分片上传和 Office 转 PDF(后续按需补充)。
|
||||
*/
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FileServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private UpFileRepository upFileRepository;
|
||||
|
||||
@Value("${file.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
private static final String DEFAULT_APP_TYPE = "notice";
|
||||
|
||||
// ============================ 上传 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UpFileVo upload(MultipartFile file, String appType, Integer topicId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new NoticeException("上传文件不得为空");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
if (!StringUtils.hasText(originalName)) {
|
||||
throw new NoticeException("文件名为空");
|
||||
}
|
||||
// 提取后缀
|
||||
String ext = "";
|
||||
int dotIdx = originalName.lastIndexOf('.');
|
||||
if (dotIdx >= 0) {
|
||||
ext = originalName.substring(dotIdx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 构造存储子路径:{appType}/{yyyyMMdd}/{uuid}.{ext}
|
||||
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String savedName = UUID.randomUUID().toString().replace("-", "") + (ext.isEmpty() ? "" : "." + ext);
|
||||
String relativePath = (appType == null ? DEFAULT_APP_TYPE : appType) + "/" + dateDir + "/" + savedName;
|
||||
|
||||
// 物理写入
|
||||
Path fullPath = Paths.get(uploadPath, relativePath);
|
||||
try {
|
||||
Files.createDirectories(fullPath.getParent());
|
||||
file.transferTo(fullPath.toFile());
|
||||
} catch (Exception e) {
|
||||
log.error("[upload] 文件保存失败: {}", fullPath, e);
|
||||
throw new NoticeException("文件保存失败", e);
|
||||
}
|
||||
|
||||
// 入库
|
||||
UpFile entity = new UpFile();
|
||||
entity.setTopicType(appType == null ? DEFAULT_APP_TYPE : appType);
|
||||
entity.setTopicId(topicId);
|
||||
entity.setFileTrueName(originalName);
|
||||
entity.setFileName(relativePath);
|
||||
entity.setFileSize((int) file.getSize());
|
||||
entity.setFileType(ext);
|
||||
upFileRepository.save(entity);
|
||||
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
// ============================ 绑定主题 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void bindToTopic(List<Integer> fileIds, Integer topicId) {
|
||||
if (fileIds == null || fileIds.isEmpty() || topicId == null) {
|
||||
return;
|
||||
}
|
||||
for (Integer fileId : fileIds) {
|
||||
upFileRepository.findById(fileId).ifPresent(f -> {
|
||||
f.setTopicId(topicId);
|
||||
upFileRepository.save(f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 查询附件列表 ============================
|
||||
|
||||
@Override
|
||||
public List<UpFileVo> listByTopic(String appType, Integer topicId) {
|
||||
String type = appType == null ? DEFAULT_APP_TYPE : appType;
|
||||
return upFileRepository.findByTopicTypeAndTopicId(type, topicId).stream()
|
||||
.map(this::toVo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============================ 下载 ============================
|
||||
|
||||
@Override
|
||||
public void download(Integer fileId, HttpServletResponse response) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new NoticeException("文件不存在: " + fileId));
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
if (!Files.exists(fullPath)) {
|
||||
throw new NoticeException("物理文件不存在: " + file.getFileName());
|
||||
}
|
||||
try (FileInputStream fis = new FileInputStream(fullPath.toFile());
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
String encodedName = URLEncoder.encode(file.getFileTrueName(), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\"");
|
||||
response.setContentLengthLong(file.getFileSize());
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
os.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("[download] 文件下载失败: {}", file.getFileName(), e);
|
||||
throw new NoticeException("文件下载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ 删除 ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer fileId) {
|
||||
UpFile file = upFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new NoticeException("文件不存在: " + fileId));
|
||||
// 删物理文件
|
||||
Path fullPath = Paths.get(uploadPath, file.getFileName());
|
||||
try {
|
||||
Files.deleteIfExists(fullPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("[delete] 物理文件删除失败: {}(数据库记录仍会删除)", fullPath, e);
|
||||
}
|
||||
// 删数据库记录
|
||||
upFileRepository.delete(file);
|
||||
}
|
||||
|
||||
// ============================ 转换 ============================
|
||||
|
||||
private UpFileVo toVo(UpFile entity) {
|
||||
UpFileVo vo = new UpFileVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+44
-3
@@ -4,7 +4,7 @@ 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 com.yunhai.notice.service.ISmsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -25,9 +25,9 @@ import java.util.stream.Collectors;
|
||||
* Java版改为从本地 sms_module + sms_school_config 表管理。
|
||||
*/
|
||||
@Service
|
||||
public class SmsServiceImpl implements SmsService {
|
||||
public class SmsService implements ISmsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SmsServiceImpl.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(SmsService.class);
|
||||
|
||||
@Autowired
|
||||
private SmsModuleRepository smsModuleRepository;
|
||||
@@ -240,4 +240,45 @@ public class SmsServiceImpl implements SmsService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ setschsms (JSON 解析) ============================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setSchSmsFromJson(String itemsModuJson) {
|
||||
Integer schSerID = extractJsonField(itemsModuJson, "SchSerID");
|
||||
Integer moduID = extractJsonField(itemsModuJson, "ModuID");
|
||||
Boolean isSms = extractJsonBool(itemsModuJson, "IsSms");
|
||||
setSchSms(schSerID, moduID, isSms);
|
||||
}
|
||||
|
||||
// ============================ 辅助方法 ============================
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取整数字段。
|
||||
*/
|
||||
private Integer extractJsonField(String json, String field) {
|
||||
if (json == null) return null;
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*\"?(-?\\d+)\"?";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Integer.parseInt(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串中提取布尔字段。
|
||||
*/
|
||||
private Boolean extractJsonBool(String json, String field) {
|
||||
if (json == null) return null;
|
||||
String pattern = "\"" + field + "\"\\s*:\\s*(true|false)";
|
||||
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher m = p.matcher(json);
|
||||
if (m.find()) {
|
||||
return Boolean.parseBoolean(m.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+16
-15
@@ -1,13 +1,14 @@
|
||||
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 com.yunhai.notice.service.ITemplateService;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -24,7 +25,7 @@ import java.util.stream.Collectors;
|
||||
* 对应 C# TemplateController + Template.cs。
|
||||
*/
|
||||
@Service
|
||||
public class TemplateServiceImpl implements TemplateService {
|
||||
public class TemplateService implements ITemplateService {
|
||||
|
||||
@Autowired
|
||||
private TemplateTypeRepository templateTypeRepository;
|
||||
@@ -63,7 +64,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
@Transactional
|
||||
public void editType(Integer typeid, String typeName) {
|
||||
if (!StringUtils.hasText(typeName)) {
|
||||
throw new NoticeException("类型名称不能为空!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "类型名称不能为空!");
|
||||
}
|
||||
// 重名校验
|
||||
boolean exists;
|
||||
@@ -73,7 +74,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
exists = templateTypeRepository.existsByTypeNameAndIdNot(typeName, typeid);
|
||||
}
|
||||
if (exists) {
|
||||
throw new NoticeException("类型名称已存在!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "类型名称已存在!");
|
||||
}
|
||||
|
||||
if (typeid == null || typeid == 0) {
|
||||
@@ -90,7 +91,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
} else {
|
||||
// 编辑
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new NoticeException("类型不存在!"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
|
||||
type.setTypeName(typeName);
|
||||
templateTypeRepository.save(type);
|
||||
}
|
||||
@@ -102,14 +103,14 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
@Transactional
|
||||
public void delType(Integer typeid) {
|
||||
if (typeid == null) {
|
||||
throw new NoticeException("类型不存在!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "类型不存在!");
|
||||
}
|
||||
TemplateType type = templateTypeRepository.findById(typeid)
|
||||
.orElseThrow(() -> new NoticeException("类型不存在!"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "类型不存在!"));
|
||||
// 校验是否有模板引用此类型
|
||||
long refCount = noticeTemplateRepository.countByTypeId(typeid);
|
||||
if (refCount > 0) {
|
||||
throw new NoticeException("类型存在模板!");
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "类型存在模板!");
|
||||
}
|
||||
templateTypeRepository.delete(type);
|
||||
}
|
||||
@@ -174,10 +175,10 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
@Transactional
|
||||
public void editTemplate(Integer serID, String title, String content, Integer typeId, Integer cover) {
|
||||
if (!StringUtils.hasText(title)) {
|
||||
throw new NoticeException("模板标题不能为空!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "模板标题不能为空!");
|
||||
}
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new NoticeException("模板内容不能为空!");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "模板内容不能为空!");
|
||||
}
|
||||
|
||||
if (serID == null || serID == 0) {
|
||||
@@ -192,7 +193,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
} else {
|
||||
// 编辑
|
||||
NoticeTemplate template = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("模板不存在!"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
|
||||
template.setTitle(title);
|
||||
template.setContent(content);
|
||||
template.setTypeId(typeId);
|
||||
@@ -207,7 +208,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
@Transactional
|
||||
public void delTemplate(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new NoticeException("参数错误");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
noticeTemplateRepository.findById(serID).ifPresent(noticeTemplateRepository::delete);
|
||||
}
|
||||
@@ -217,10 +218,10 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
@Override
|
||||
public Map<String, Object> getTemplateFist(Integer serID) {
|
||||
if (serID == null) {
|
||||
throw new NoticeException("参数错误");
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||
}
|
||||
NoticeTemplate t = noticeTemplateRepository.findById(serID)
|
||||
.orElseThrow(() -> new NoticeException("模板不存在!"));
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "模板不存在!"));
|
||||
|
||||
// 加载类型名
|
||||
String typeName = "";
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.yunhai.notice.service.impl;
|
||||
|
||||
import com.yunhai.notice.model.SchTeaList;
|
||||
import com.yunhai.notice.service.IClassService;
|
||||
import com.yunhai.notice.service.IUserCenterService;
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户中心门面服务实现。
|
||||
* <p>原 BaseApiController.getSchoolAll 的组树组装、教师挂组、递归建树、排序逻辑整体下沉至此;
|
||||
* GetGroupUser 空桩改为显式报错(未接入用户中心对应开放接口时提示,而非静默返回空列表)。
|
||||
* 跨仓调用统一收敛到 {@link YschBaseOpenApi} 门面,Controller 不再直接静态调用。</p>
|
||||
*/
|
||||
@Service
|
||||
public class UserCenterService implements IUserCenterService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(UserCenterService.class);
|
||||
|
||||
/** 学生组(UserBase GroupTypeEnum.STUDENT_GROUP),组树中剔除 */
|
||||
private static final int GROUP_TYPE_STUDENT = 8;
|
||||
|
||||
@Autowired
|
||||
private IClassService classService;
|
||||
|
||||
// ============================ 学校组织树 ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getSchoolAllTree(Integer schId) {
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
|
||||
}
|
||||
|
||||
// 1. userbase 组树(整棵学校子树),剔除学生组
|
||||
List<Map<String, Object>> flatNodes = new ArrayList<>();
|
||||
BoaGroup root = YschBaseOpenApi.getGroupById(schId, true);
|
||||
if (root == null) {
|
||||
log.warn("[getSchoolAll] 用户中心未找到学校组, schId={}", schId);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
flattenGroup(root, 0, flatNodes);
|
||||
|
||||
// 2. bschclass 全部教师,按 GroupIds 挂到对应组下;无有效组的兜底挂学校根节点
|
||||
Set<Integer> treeGroupIds = new HashSet<>();
|
||||
for (Map<String, Object> node : flatNodes) {
|
||||
treeGroupIds.add((Integer) node.get("id"));
|
||||
}
|
||||
Map<Integer, SchTeaList> teacherMap = new LinkedHashMap<>();
|
||||
Map<Integer, Set<Integer>> teacherPids = new LinkedHashMap<>();
|
||||
List<SchTeaList> teachers = classService.getSchTeaList(schId);
|
||||
for (SchTeaList tea : teachers) {
|
||||
if (tea.getUserTeaSerID() == null) {
|
||||
continue;
|
||||
}
|
||||
// 同一教师多行(多组/接口重复数据)合并,按 (教师, 组) 去重
|
||||
teacherMap.putIfAbsent(tea.getUserTeaSerID(), tea);
|
||||
// 接口现行字段为单个 GroupID,兼容旧字段 GroupIds
|
||||
List<Integer> teaGroupIds = tea.getGroupIds();
|
||||
if ((teaGroupIds == null || teaGroupIds.isEmpty()) && tea.getGroupId() != null) {
|
||||
teaGroupIds = Collections.singletonList(tea.getGroupId());
|
||||
}
|
||||
if (teaGroupIds != null) {
|
||||
for (Integer gid : teaGroupIds) {
|
||||
if (gid != null && treeGroupIds.contains(gid)) {
|
||||
teacherPids.computeIfAbsent(tea.getUserTeaSerID(), k -> new HashSet<>()).add(gid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Integer, SchTeaList> entry : teacherMap.entrySet()) {
|
||||
int teaId = entry.getKey();
|
||||
SchTeaList tea = entry.getValue();
|
||||
Set<Integer> pids = teacherPids.get(teaId);
|
||||
if (pids == null || pids.isEmpty()) {
|
||||
// 教师所属组均被剔除(如学生组)或无组信息,挂学校根节点
|
||||
pids = new HashSet<>(Collections.singletonList(treeGroupIds.contains(schId) ? schId : 0));
|
||||
}
|
||||
for (Integer pid : pids) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", teaId);
|
||||
node.put("name", tea.getTeaName() != null ? tea.getTeaName() : "");
|
||||
node.put("pid", pid);
|
||||
node.put("no", tea.getTeaNo() != null ? tea.getTeaNo() : 0);
|
||||
node.put("type", 2);
|
||||
node.put("isParent", false);
|
||||
node.put("photo", "");
|
||||
flatNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 扁平节点递归组装为树
|
||||
return formatUserTree(flatNodes, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 BoaGroup 子树扁平化为组节点列表(type=1);学生组(type=8)整棵剔除。
|
||||
* isParent=true 标识组节点,前端 ztree 据此渲染为文件夹。
|
||||
*/
|
||||
private void flattenGroup(BoaGroup group, int pid, List<Map<String, Object>> nodes) {
|
||||
if (group == null || group.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (group.getType() != null && group.getType() == GROUP_TYPE_STUDENT) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", group.getId());
|
||||
node.put("name", group.getName() != null ? group.getName() : "");
|
||||
node.put("pid", pid);
|
||||
node.put("no", group.getSort() != null ? group.getSort() : 0);
|
||||
node.put("type", 1);
|
||||
node.put("isParent", true);
|
||||
node.put("photo", "");
|
||||
nodes.add(node);
|
||||
|
||||
if (group.getChildren() != null) {
|
||||
for (BoaGroup child : group.getChildren()) {
|
||||
flattenGroup(child, group.getId(), nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建用户树(对应 C# HttpUserApi.FormatUserTree)。
|
||||
* 根层取 pid=0 或父节点不在列表中的节点;type=1 且无子节点的空组剔除。
|
||||
*/
|
||||
private List<Map<String, Object>> formatUserTree(List<Map<String, Object>> treeList, int pid) {
|
||||
List<Map<String, Object>> data;
|
||||
if (pid == 0) {
|
||||
Set<Integer> allIds = new HashSet<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
allIds.add((Integer) n.get("id"));
|
||||
}
|
||||
data = new ArrayList<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
int p = (Integer) n.get("pid");
|
||||
if (p == 0 || !allIds.contains(p)) {
|
||||
data.add(n);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = new ArrayList<>();
|
||||
for (Map<String, Object> n : treeList) {
|
||||
if ((Integer) n.get("pid") == pid) {
|
||||
data.add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> item : data) {
|
||||
int itemId = (Integer) item.get("id");
|
||||
int itemType = (Integer) item.get("type");
|
||||
|
||||
List<Map<String, Object>> children = formatUserTree(treeList, itemId);
|
||||
|
||||
// type=1(组) 且无子节点 → 不加入(C# 逻辑)
|
||||
if (itemType == 1 && children.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> node = new LinkedHashMap<>(item);
|
||||
// 仅组节点携带 children;叶子不带该字段,避免 ztree 误判 isParent
|
||||
if (itemType == 1) {
|
||||
node.put("children", children);
|
||||
}
|
||||
result.add(node);
|
||||
}
|
||||
|
||||
// 按 no 排序(数字优先,无法解析的排最后)
|
||||
result.sort(Comparator.comparingInt(n -> {
|
||||
Object no = n.get("no");
|
||||
if (no instanceof Integer) {
|
||||
return (Integer) no;
|
||||
}
|
||||
if (no instanceof String) {
|
||||
try {
|
||||
return Integer.parseInt((String) no);
|
||||
} catch (NumberFormatException e) {
|
||||
return 9999;
|
||||
}
|
||||
}
|
||||
return 9999;
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ 组内用户 ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getGroupUser(Integer gid, Integer role) {
|
||||
if (gid == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "组ID不得为空");
|
||||
}
|
||||
log.info("[getGroupUser] gid={}, role={}, 用户中心组内用户开放接口尚未接入", gid, role);
|
||||
// 显式报错而非静默返回空列表:原空桩会让前端误以为组内无用户
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "组内用户查询接口暂未接入用户中心,请联系管理员配置后重试");
|
||||
}
|
||||
|
||||
// ============================ 用户模块树 ============================
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getUserModuleTree(Integer userId, Integer schId) {
|
||||
if (userId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "用户ID不得为空");
|
||||
}
|
||||
List<BoaModule> modules = YschBaseOpenApi.getUserModule(userId, schId);
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 扁平 BoaModule → 树结构(modId/page/children),对齐前端 layout 期望
|
||||
return buildModuleTree(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将扁平模块列表组装为树(对应原 NoticeController.buildModuleTree)。
|
||||
* 顶层取 parentId=0 或父节点不在列表中的节点,子节点按 orderNumber 排序。
|
||||
*/
|
||||
private List<Map<String, Object>> buildModuleTree(List<BoaModule> modules) {
|
||||
Map<Integer, BoaModule> moduleMap = modules.stream()
|
||||
.collect(Collectors.toMap(BoaModule::getId, m -> m, (a, b) -> a));
|
||||
|
||||
// 每个模块节点(含 children 占位)
|
||||
Map<Integer, Map<String, Object>> nodeMap = new LinkedHashMap<>();
|
||||
for (BoaModule m : modules) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("id", m.getId());
|
||||
node.put("modId", m.getId());
|
||||
node.put("name", m.getName());
|
||||
node.put("parentId", m.getParentId() != null ? m.getParentId() : 0);
|
||||
node.put("page", m.getPath() != null ? m.getPath() : "");
|
||||
node.put("level", m.getLevel() != null ? m.getLevel() : 0);
|
||||
node.put("icon", m.getIcon() != null ? m.getIcon() : "");
|
||||
node.put("orderNumber", m.getSortNo() != null ? m.getSortNo() : 0);
|
||||
node.put("usable", true);
|
||||
node.put("children", new ArrayList<>());
|
||||
nodeMap.put(m.getId(), node);
|
||||
}
|
||||
|
||||
// 挂父子关系
|
||||
List<Map<String, Object>> roots = new ArrayList<>();
|
||||
for (BoaModule m : modules) {
|
||||
Map<String, Object> node = nodeMap.get(m.getId());
|
||||
int parentId = m.getParentId() != null ? m.getParentId() : 0;
|
||||
if (parentId == 0 || !moduleMap.containsKey(parentId)) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) nodeMap.get(parentId).get("children");
|
||||
children.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 全树按 orderNumber 排序
|
||||
sortTree(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void sortTree(List<Map<String, Object>> nodes) {
|
||||
nodes.sort(Comparator.comparingInt(n -> (Integer) n.get("orderNumber")));
|
||||
for (Map<String, Object> node : nodes) {
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
|
||||
if (!children.isEmpty()) {
|
||||
sortTree(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.notice.service.sms;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短信发送默认空实现:仅记录日志,真实短信通道接入前的占位。
|
||||
*/
|
||||
@Component
|
||||
public class DefaultSmsSender implements SmsSender {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DefaultSmsSender.class);
|
||||
|
||||
@Override
|
||||
public void sendNoticeSms(Integer noticeId, String title, List<String> receiverAccounts) {
|
||||
log.info("[SmsSender] 预留短信能力:通知 {} 标题「{}」,接收人 {} 人(暂未接入真实短信通道)",
|
||||
noticeId, title, receiverAccounts == null ? 0 : receiverAccounts.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.notice.service.sms;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知短信发送抽象接口。
|
||||
* <p>
|
||||
* 预留未来短信能力:发通知且开启短信(smsFlag=1)时调用;
|
||||
* 当前仅提供默认空实现(记录日志),真实短信通道后续接入。
|
||||
* </p>
|
||||
*/
|
||||
public interface SmsSender {
|
||||
|
||||
/**
|
||||
* 发送通知短信。
|
||||
*
|
||||
* @param noticeId 通知ID
|
||||
* @param title 通知标题
|
||||
* @param receiverAccounts 接收人账号列表(空表示全员通知)
|
||||
*/
|
||||
void sendNoticeSms(Integer noticeId, String title, List<String> receiverAccounts);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
server:
|
||||
port: 8070
|
||||
servlet:
|
||||
context-path: /notice
|
||||
|
||||
spring:
|
||||
mvc:
|
||||
@@ -47,11 +45,6 @@ spring:
|
||||
file:
|
||||
upload-path: D:/data/upload
|
||||
|
||||
# 定时发送任务
|
||||
timing-send:
|
||||
enabled: true
|
||||
cron: "0 * * * * ?" # 每分钟检查一次
|
||||
|
||||
# 短信模块配置
|
||||
sms:
|
||||
enabled: true
|
||||
@@ -75,3 +68,5 @@ yh:
|
||||
enabled: true
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
user-center-url: http://base.yunhaixingtong.com/userbase
|
||||
# 班级管理(bschclass)服务基址
|
||||
bschclass: http://base.yunhaixingtong.com/bschclass
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
server:
|
||||
port: 8070
|
||||
|
||||
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
|
||||
|
||||
# 短信模块配置
|
||||
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
|
||||
# 班级管理(bschclass)服务基址
|
||||
bschclass: http://base.yunhaixingtong.com/bschclass
|
||||
@@ -0,0 +1,16 @@
|
||||
-- ============================================================
|
||||
-- notice 表补 sch_id 列(多租户隔离)
|
||||
-- 决策 D3:发布时从 BoaUser.currentRole.schId 填充,查询时按校过滤
|
||||
--
|
||||
-- 对应实体:Notice.schId / NoticeRepository 多租户统计方法
|
||||
--
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑
|
||||
-- MySQL 5.7+ 兼容
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 加列
|
||||
ALTER TABLE `notice`
|
||||
ADD COLUMN `sch_id` INT NULL COMMENT '学校ID(多租户隔离:发布时从当前用户角色填充)';
|
||||
|
||||
-- 2) 加索引(用于按学校过滤查询)
|
||||
CREATE INDEX `idx_notice_sch_id` ON `notice` (`sch_id`);
|
||||
@@ -0,0 +1,159 @@
|
||||
-- ============================================================
|
||||
-- 存量数据回填脚本
|
||||
-- 1. notice.sch_id:按发布人所属学校回填(从 sys_user_role_rel 取 main=1 的 sch_id)
|
||||
-- 2. notice_receiver:从 notice.receive_list JSON 字符串展开为关联表行
|
||||
--
|
||||
-- 幂等设计:可重复执行,已存在的数据不会重复插入
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑
|
||||
-- MySQL 5.7+ 兼容
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- Part 1:回填 notice.sch_id
|
||||
-- ============================================================
|
||||
|
||||
-- 方式一:如果 base_user / sys_user 表有 sch_id 字段,直接关联回填
|
||||
-- 注意:需根据实际平台库表结构调整关联条件
|
||||
UPDATE `notice` n
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT u.login_name, u.sch_id
|
||||
FROM base_user u
|
||||
WHERE u.sch_id IS NOT NULL
|
||||
) t ON n.publisher_account = t.login_name
|
||||
SET n.sch_id = t.sch_id
|
||||
WHERE n.sch_id IS NULL
|
||||
AND n.publisher_account IS NOT NULL
|
||||
AND n.publisher_account != '';
|
||||
|
||||
-- 方式二(备用):如果用户学校关系在 sys_user_role_rel 表
|
||||
-- 取每个用户的主身份学校(is_main=1),回填 sch_id
|
||||
-- 取消注释后执行(视实际库表结构选用)
|
||||
--
|
||||
-- UPDATE `notice` n
|
||||
-- INNER JOIN (
|
||||
-- SELECT r.account, r.sch_id
|
||||
-- FROM sys_user_role_rel r
|
||||
-- WHERE r.is_main = 1
|
||||
-- AND r.sch_id IS NOT NULL
|
||||
-- ) t ON n.publisher_account = t.account
|
||||
-- SET n.sch_id = t.sch_id
|
||||
-- WHERE n.sch_id IS NULL
|
||||
-- AND n.publisher_account IS NOT NULL
|
||||
-- AND n.publisher_account != '';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Part 2:从 notice.receive_list 展开到 notice_receiver 关联表
|
||||
-- ============================================================
|
||||
|
||||
-- 说明:
|
||||
-- - notice.receive_list 存储格式有两种:
|
||||
-- 1) "ALL"(全员通知)—— 不写明细行,跳过
|
||||
-- 2) JSON 数组 ["account1","account2",...] 或逗号分隔串
|
||||
-- - 本脚本使用 MySQL JSON 函数解析(MySQL 5.7+ 支持 JSON_EXTRACT)
|
||||
-- - 对于非标准 JSON 格式(逗号分隔串),使用循环逐条插入
|
||||
-- - 幂等:每次插入前先删除该通知的旧接收人记录
|
||||
|
||||
-- 注意:MySQL 5.7 的 JSON_TABLE 不可用,这里用存储过程逐条解析
|
||||
-- 如果 receive_list 是标准 JSON 数组格式,可以直接用 JSON_EXTRACT
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
-- 存储过程:展开单个通知的接收人列表
|
||||
DROP PROCEDURE IF EXISTS `expand_notice_receivers`$$
|
||||
CREATE PROCEDURE `expand_notice_receivers`()
|
||||
BEGIN
|
||||
DECLARE v_done INT DEFAULT 0;
|
||||
DECLARE v_notice_id INT;
|
||||
DECLARE v_receive_list TEXT;
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT id, receive_list
|
||||
FROM notice
|
||||
WHERE receive_list IS NOT NULL
|
||||
AND TRIM(receive_list) != ''
|
||||
AND UPPER(TRIM(receive_list)) != 'ALL'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM notice_receiver WHERE notice_id = notice.id LIMIT 1
|
||||
);
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
|
||||
|
||||
OPEN cur;
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_notice_id, v_receive_list;
|
||||
IF v_done = 1 THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- 清理旧数据(幂等)
|
||||
DELETE FROM notice_receiver WHERE notice_id = v_notice_id;
|
||||
|
||||
-- 尝试 JSON 数组解析
|
||||
-- receive_list 格式: ["account1","account2"] 或 account1,account2
|
||||
SET v_receive_list = TRIM(v_receive_list);
|
||||
|
||||
-- 去除首尾方括号(如有)
|
||||
IF LEFT(v_receive_list, 1) = '[' AND RIGHT(v_receive_list, 1) = ']' THEN
|
||||
SET v_receive_list = SUBSTRING(v_receive_list, 2, CHAR_LENGTH(v_receive_list) - 2);
|
||||
END IF;
|
||||
|
||||
-- 替换引号为空,按逗号分隔
|
||||
SET v_receive_list = REPLACE(REPLACE(v_receive_list, '"', ''), '''', '');
|
||||
|
||||
-- 逐条插入
|
||||
-- 简单实现:用 FIND_IN_SET 不适用于变量,改用临时表
|
||||
BEGIN
|
||||
DECLARE v_pos INT DEFAULT 1;
|
||||
DECLARE v_account VARCHAR(64);
|
||||
DECLARE v_remaining TEXT;
|
||||
SET v_remaining = v_receive_list;
|
||||
|
||||
WHILE v_remaining IS NOT NULL AND TRIM(v_remaining) != '' DO
|
||||
IF LOCATE(',', v_remaining) > 0 THEN
|
||||
SET v_account = TRIM(SUBSTRING_INDEX(v_remaining, ',', 1));
|
||||
SET v_remaining = SUBSTRING(v_remaining, LOCATE(',', v_remaining) + 1);
|
||||
ELSE
|
||||
SET v_account = TRIM(v_remaining);
|
||||
SET v_remaining = NULL;
|
||||
END IF;
|
||||
|
||||
IF v_account != '' AND v_account IS NOT NULL THEN
|
||||
INSERT INTO notice_receiver (notice_id, account, receive_type, create_time)
|
||||
VALUES (v_notice_id, v_account, 1, NOW());
|
||||
END IF;
|
||||
END WHILE;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 执行存储过程
|
||||
CALL expand_notice_receivers();
|
||||
|
||||
-- 清理临时存储过程
|
||||
DROP PROCEDURE IF EXISTS `expand_notice_receivers`;
|
||||
|
||||
-- ============================================================
|
||||
-- 回填结果验证查询(执行后手动检查)
|
||||
-- ============================================================
|
||||
|
||||
-- 检查1:sch_id 回填覆盖率
|
||||
-- SELECT
|
||||
-- COUNT(*) AS total,
|
||||
-- SUM(CASE WHEN sch_id IS NOT NULL THEN 1 ELSE 0 END) AS sch_id_filled,
|
||||
-- SUM(CASE WHEN sch_id IS NULL THEN 1 ELSE 0 END) AS sch_id_null
|
||||
-- FROM notice;
|
||||
|
||||
-- 检查2:notice_receiver 展开行数
|
||||
-- SELECT COUNT(*) FROM notice_receiver;
|
||||
|
||||
-- 检查3:未展开的通知(receive_list 非空非 ALL 但 receiver 表无记录)
|
||||
-- SELECT n.id, n.receive_list
|
||||
-- FROM notice n
|
||||
-- LEFT JOIN notice_receiver r ON r.notice_id = n.id
|
||||
-- WHERE n.receive_list IS NOT NULL
|
||||
-- AND TRIM(n.receive_list) != ''
|
||||
-- AND UPPER(TRIM(n.receive_list)) != 'ALL'
|
||||
-- AND r.id IS NULL;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- ============================================================
|
||||
-- notice_receiver 建表(替代 notice.receive_list JSON 字符串存储)
|
||||
-- 决策 D2-B:拆关联表,支撑阅读情况/催收/统计 SQL 化
|
||||
--
|
||||
-- 对应实体:com.yunhai.notice.entity.NoticeReceiver
|
||||
-- 索引:idx_notice_id(notice_id) + idx_account(account)
|
||||
--
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑
|
||||
-- MySQL 5.7+ 兼容
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `notice_receiver` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||
`notice_id` INT NOT NULL COMMENT '通知ID(关联 notice.id)',
|
||||
`account` VARCHAR(64) NOT NULL COMMENT '接收人登录账号',
|
||||
`receive_type` INT NOT NULL DEFAULT 1 COMMENT '接收类型:1 接收 / 2 抄送',
|
||||
`create_by` VARCHAR(64) NULL COMMENT '创建人登录账号(审计填充)',
|
||||
`create_time` DATETIME NULL COMMENT '创建时间(审计填充)',
|
||||
`update_by` VARCHAR(64) NULL COMMENT '更新人登录账号(审计填充)',
|
||||
`update_time` DATETIME NULL COMMENT '更新时间(审计填充)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_notice_id` (`notice_id`),
|
||||
KEY `idx_account` (`account`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知接收人关联表';
|
||||
Reference in New Issue
Block a user